mirror of
				https://github.com/imgproxy/imgproxy.git
				synced 2025-10-30 23:08:02 +02:00 
			
		
		
		
	Add client features detector
This commit is contained in:
		
							
								
								
									
										51
									
								
								clientfeatures/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								clientfeatures/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| package clientfeatures | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/ensure" | ||||
| 	"github.com/imgproxy/imgproxy/v3/env" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	IMGPROXY_AUTO_WEBP           = env.Describe("IMGPROXY_AUTO_WEBP", "boolean") | ||||
| 	IMGPROXY_AUTO_AVIF           = env.Describe("IMGPROXY_AUTO_AVIF", "boolean") | ||||
| 	IMGPROXY_AUTO_JXL            = env.Describe("IMGPROXY_AUTO_JXL", "boolean") | ||||
| 	IMGPROXY_ENFORCE_WEBP        = env.Describe("IMGPROXY_ENFORCE_WEBP", "boolean") | ||||
| 	IMGPROXY_ENFORCE_AVIF        = env.Describe("IMGPROXY_ENFORCE_AVIF", "boolean") | ||||
| 	IMGPROXY_ENFORCE_JXL         = env.Describe("IMGPROXY_ENFORCE_JXL", "boolean") | ||||
| 	IMGPROXY_ENABLE_CLIENT_HINTS = env.Describe("IMGPROXY_ENABLE_CLIENT_HINTS", "boolean") | ||||
| ) | ||||
|  | ||||
| // Config holds configuration for response writer | ||||
| type Config struct { | ||||
| 	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 | ||||
|  | ||||
| 	EnableClientHints bool // Whether to enable client hints support | ||||
| } | ||||
|  | ||||
| // NewDefaultConfig returns a new Config instance with default values. | ||||
| func NewDefaultConfig() Config { | ||||
| 	return Config{} // All features disabled by default | ||||
| } | ||||
|  | ||||
| // LoadConfigFromEnv overrides configuration variables from environment | ||||
| func LoadConfigFromEnv(c *Config) (*Config, error) { | ||||
| 	c = ensure.Ensure(c, NewDefaultConfig) | ||||
|  | ||||
| 	err := errors.Join( | ||||
| 		env.Bool(&c.AutoWebp, IMGPROXY_AUTO_WEBP), | ||||
| 		env.Bool(&c.EnforceWebp, IMGPROXY_ENFORCE_WEBP), | ||||
| 		env.Bool(&c.AutoAvif, IMGPROXY_AUTO_AVIF), | ||||
| 		env.Bool(&c.EnforceAvif, IMGPROXY_ENFORCE_AVIF), | ||||
| 		env.Bool(&c.AutoJxl, IMGPROXY_AUTO_JXL), | ||||
| 		env.Bool(&c.EnforceJxl, IMGPROXY_ENFORCE_JXL), | ||||
| 	) | ||||
|  | ||||
| 	return c, err | ||||
| } | ||||
							
								
								
									
										99
									
								
								clientfeatures/detector.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								clientfeatures/detector.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| package clientfeatures | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/httpheaders" | ||||
| ) | ||||
|  | ||||
| // Maximum supported DPR from client hints | ||||
| const maxClientHintDPR = 8 | ||||
|  | ||||
| // Detector detects client features from request headers | ||||
| type Detector struct { | ||||
| 	config *Config | ||||
| 	vary   string | ||||
| } | ||||
|  | ||||
| // NewDetector creates a new Detector instance | ||||
| func NewDetector(config *Config) *Detector { | ||||
| 	vary := make([]string, 0, 5) | ||||
|  | ||||
| 	if config.AutoWebp || config.EnforceWebp || | ||||
| 		config.AutoAvif || config.EnforceAvif || | ||||
| 		config.AutoJxl || config.EnforceJxl { | ||||
| 		vary = append(vary, httpheaders.Accept) | ||||
| 	} | ||||
|  | ||||
| 	if config.EnableClientHints { | ||||
| 		vary = append( | ||||
| 			vary, | ||||
| 			httpheaders.SecChDpr, httpheaders.Dpr, httpheaders.SecChWidth, httpheaders.Width, | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	return &Detector{ | ||||
| 		config: config, | ||||
| 		vary:   strings.Join(vary, ", "), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Features detects client features from HTTP headers | ||||
| func (d *Detector) Features(header http.Header) Features { | ||||
| 	var f Features | ||||
|  | ||||
| 	headerAccept := header.Get("Accept") | ||||
|  | ||||
| 	if (d.config.AutoWebp || d.config.EnforceWebp) && strings.Contains(headerAccept, "image/webp") { | ||||
| 		f.PreferWebP = true | ||||
| 		f.EnforceWebP = d.config.EnforceWebp | ||||
| 	} | ||||
|  | ||||
| 	if (d.config.AutoAvif || d.config.EnforceAvif) && strings.Contains(headerAccept, "image/avif") { | ||||
| 		f.PreferAvif = true | ||||
| 		f.EnforceAvif = d.config.EnforceAvif | ||||
| 	} | ||||
|  | ||||
| 	if (d.config.AutoJxl || d.config.EnforceJxl) && strings.Contains(headerAccept, "image/jxl") { | ||||
| 		f.PreferJxl = true | ||||
| 		f.EnforceJxl = d.config.EnforceJxl | ||||
| 	} | ||||
|  | ||||
| 	if !d.config.EnableClientHints { | ||||
| 		return f | ||||
| 	} | ||||
| 	for _, key := range []string{httpheaders.SecChDpr, httpheaders.Dpr} { | ||||
| 		val := header.Get(key) | ||||
| 		if len(val) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if d, err := strconv.ParseFloat(val, 64); err == nil && (d > 0 && d <= maxClientHintDPR) { | ||||
| 			f.ClientHintsDPR = d | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for _, key := range []string{httpheaders.SecChWidth, httpheaders.Width} { | ||||
| 		val := header.Get(key) | ||||
| 		if len(val) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if w, err := strconv.Atoi(val); err == nil && w > 0 { | ||||
| 			f.ClientHintsWidth = w | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return f | ||||
| } | ||||
|  | ||||
| // SetVary sets the Vary header value based on enabled features | ||||
| func (d *Detector) SetVary(header http.Header) { | ||||
| 	if len(d.vary) > 0 { | ||||
| 		header.Set(httpheaders.Vary, d.vary) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										436
									
								
								clientfeatures/detector_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										436
									
								
								clientfeatures/detector_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,436 @@ | ||||
| package clientfeatures | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/suite" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/httpheaders" | ||||
| 	"github.com/imgproxy/imgproxy/v3/logger" | ||||
| ) | ||||
|  | ||||
| type detectorTestCase struct { | ||||
| 	name     string | ||||
| 	config   Config | ||||
| 	header   map[string]string | ||||
| 	expected Features | ||||
| } | ||||
|  | ||||
| type ClientFeaturesDetectorSuite struct { | ||||
| 	suite.Suite | ||||
| } | ||||
|  | ||||
| func (s *ClientFeaturesDetectorSuite) SetupSuite() { | ||||
| 	logger.Mute() | ||||
| } | ||||
|  | ||||
| func (s *ClientFeaturesDetectorSuite) TearDownSuite() { | ||||
| 	logger.Unmute() | ||||
| } | ||||
|  | ||||
| func (s *ClientFeaturesDetectorSuite) runTestCases(testCases []detectorTestCase) { | ||||
| 	for _, tc := range testCases { | ||||
| 		s.Run(tc.name, func() { | ||||
| 			detector := NewDetector(&tc.config) | ||||
|  | ||||
| 			header := make(http.Header) | ||||
| 			for k, v := range tc.header { | ||||
| 				header.Set(k, v) | ||||
| 			} | ||||
|  | ||||
| 			features := detector.Features(header) | ||||
| 			s.Require().Equal(tc.expected, features) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *ClientFeaturesDetectorSuite) TestFeaturesAutoFormats() { | ||||
| 	s.runTestCases([]detectorTestCase{ | ||||
| 		{ | ||||
| 			name: "AutoWebP_ConainsWebP", | ||||
| 			config: Config{ | ||||
| 				AutoWebp: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/webp,image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				PreferWebP: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "AutoWebP_DoesNotContainWebP", | ||||
| 			config: Config{ | ||||
| 				AutoWebp: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "EnforceWebP_ContainsWebP", | ||||
| 			config: Config{ | ||||
| 				EnforceWebp: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/webp,image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				PreferWebP:  true, | ||||
| 				EnforceWebP: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "EnforceWebP_DoesNotContainWebP", | ||||
| 			config: Config{ | ||||
| 				EnforceWebp: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "AutoAvif_ContainsAvif", | ||||
| 			config: Config{ | ||||
| 				AutoAvif: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/avif,image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				PreferAvif: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "AutoAvif_DoesNotContainAvif", | ||||
| 			config: Config{ | ||||
| 				AutoAvif: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "EnforceAvif_ContainsAvif", | ||||
| 			config: Config{ | ||||
| 				EnforceAvif: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/avif,image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				PreferAvif:  true, | ||||
| 				EnforceAvif: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "EnforceAvif_DoesNotContainAvif", | ||||
| 			config: Config{ | ||||
| 				EnforceAvif: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "AutoJXL_ContainsJXL", | ||||
| 			config: Config{ | ||||
| 				AutoJxl: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/jxl,image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				PreferJxl: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "AutoJXL_DoesNotContainJXL", | ||||
| 			config: Config{ | ||||
| 				AutoJxl: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "EnforceJXL_ContainsJXL", | ||||
| 			config: Config{ | ||||
| 				EnforceJxl: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/jxl,image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				PreferJxl:  true, | ||||
| 				EnforceJxl: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "EnforceJXL_DoesNotContainJXL", | ||||
| 			config: Config{ | ||||
| 				EnforceJxl: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "NoneEnabled_ContainsAll", | ||||
| 			config: Config{ | ||||
| 				AutoWebp:    false, | ||||
| 				EnforceWebp: false, | ||||
| 				AutoAvif:    false, | ||||
| 				EnforceAvif: false, | ||||
| 				AutoJxl:     false, | ||||
| 				EnforceJxl:  false, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Accept": "image/webp,image/avif,image/jxl,image/apng,image/*,*/*;q=0.8", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (s *ClientFeaturesDetectorSuite) TestFeaturesClientHintsDPR() { | ||||
| 	s.runTestCases([]detectorTestCase{ | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_ValidDPR", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"DPR": "1.5", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				ClientHintsDPR: 1.5, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_ValidSecChDPR", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Sec-CH-DPR": "2.0", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				ClientHintsDPR: 2.0, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_ValidDprAndSecChDPR", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"DPR":        "3.0", | ||||
| 				"Sec-CH-DPR": "2.5", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				ClientHintsDPR: 2.5, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_InvalidDPR_Negative", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"DPR": "-1.0", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_InvalidDPR_TooHigh", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"DPR": "10.0", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_InvalidDPR_NonNumeric", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"DPR": "abc", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsDisabled", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: false, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"DPR":        "2.0", | ||||
| 				"Sec-CH-DPR": "3.0", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (s *ClientFeaturesDetectorSuite) TestFeaturesClientHintsWidth() { | ||||
| 	s.runTestCases([]detectorTestCase{ | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_ValidWidth", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Width": "800", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				ClientHintsWidth: 800, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_ValidSecChWidth", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Sec-CH-Width": "1024", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				ClientHintsWidth: 1024, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_ValidWidthAndSecChWidth", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Width":        "1280", | ||||
| 				"Sec-CH-Width": "1440", | ||||
| 			}, | ||||
| 			expected: Features{ | ||||
| 				ClientHintsWidth: 1440, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_InvalidWidth_Negative", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Width": "-800", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsEnabled_InvalidWidth_NonNumeric", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Width": "abc", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "ClientHintsDisabled", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: false, | ||||
| 			}, | ||||
| 			header: map[string]string{ | ||||
| 				"Width":        "800", | ||||
| 				"Sec-CH-Width": "1024", | ||||
| 			}, | ||||
| 			expected: Features{}, | ||||
| 		}, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (s *ClientFeaturesDetectorSuite) TestSetVary() { | ||||
| 	testCases := []struct { | ||||
| 		name     string | ||||
| 		config   Config | ||||
| 		expected string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "AutoWebP_Enabled", | ||||
| 			config: Config{ | ||||
| 				AutoWebp: true, | ||||
| 			}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "EnforceWebP_Enabled", | ||||
| 			config: Config{ | ||||
| 				EnforceWebp: true, | ||||
| 			}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "AutoAvif_Enabled", | ||||
| 			config: Config{ | ||||
| 				AutoAvif: true, | ||||
| 			}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "EnforceAvif_Enabled", | ||||
| 			config: Config{ | ||||
| 				EnforceAvif: true, | ||||
| 			}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "AutoJXL_Enabled", | ||||
| 			config: Config{ | ||||
| 				AutoJxl: true, | ||||
| 			}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "EnforceJXL_Enabled", | ||||
| 			config: Config{ | ||||
| 				EnforceJxl: true, | ||||
| 			}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "EnableClientHints_Enabled", | ||||
| 			config: Config{ | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			expected: "Sec-Ch-Dpr, Dpr, Sec-Ch-Width, Width", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Combined", | ||||
| 			config: Config{ | ||||
| 				AutoWebp:          true, | ||||
| 				EnableClientHints: true, | ||||
| 			}, | ||||
| 			expected: "Accept, Sec-Ch-Dpr, Dpr, Sec-Ch-Width, Width", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		s.Run(tc.name, func() { | ||||
| 			detector := NewDetector(&tc.config) | ||||
| 			header := http.Header{} | ||||
| 			detector.SetVary(header) | ||||
| 			s.Require().Equal(tc.expected, header.Get(httpheaders.Vary)) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestClientFeaturesDetector(t *testing.T) { | ||||
| 	suite.Run(t, new(ClientFeaturesDetectorSuite)) | ||||
| } | ||||
							
								
								
									
										16
									
								
								clientfeatures/features.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								clientfeatures/features.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| package clientfeatures | ||||
|  | ||||
| // Features holds information about features extracted from HTTP request | ||||
| type Features struct { | ||||
| 	PreferWebP  bool // Whether to prefer WebP format when resulting image format is unknown | ||||
| 	EnforceWebP bool // Whether to enforce WebP format even if resulting image format is set | ||||
|  | ||||
| 	PreferAvif  bool // Whether to prefer AVIF format when resulting image format is unknown | ||||
| 	EnforceAvif bool // Whether to enforce AVIF format even if resulting image format is set | ||||
|  | ||||
| 	PreferJxl  bool // Whether to prefer JXL format when resulting image format is unknown | ||||
| 	EnforceJxl bool // Whether to enforce JXL format even if resulting image format is set | ||||
|  | ||||
| 	ClientHintsWidth int     // Client hint width | ||||
| 	ClientHintsDPR   float64 // Client hint device pixel ratio | ||||
| } | ||||
| @@ -2,6 +2,7 @@ package imgproxy | ||||
|  | ||||
| import ( | ||||
| 	"github.com/imgproxy/imgproxy/v3/auximageprovider" | ||||
| 	"github.com/imgproxy/imgproxy/v3/clientfeatures" | ||||
| 	"github.com/imgproxy/imgproxy/v3/cookies" | ||||
| 	"github.com/imgproxy/imgproxy/v3/ensure" | ||||
| 	"github.com/imgproxy/imgproxy/v3/errorreport" | ||||
| @@ -29,6 +30,7 @@ type Config struct { | ||||
| 	FallbackImage  auximageprovider.StaticConfig | ||||
| 	WatermarkImage auximageprovider.StaticConfig | ||||
| 	Fetcher        fetcher.Config | ||||
| 	ClientFeatures clientfeatures.Config | ||||
| 	Handlers       HandlerConfigs | ||||
| 	Server         server.Config | ||||
| 	Security       security.Config | ||||
| @@ -46,6 +48,7 @@ func NewDefaultConfig() Config { | ||||
| 		FallbackImage:  auximageprovider.NewDefaultStaticConfig(), | ||||
| 		WatermarkImage: auximageprovider.NewDefaultStaticConfig(), | ||||
| 		Fetcher:        fetcher.NewDefaultConfig(), | ||||
| 		ClientFeatures: clientfeatures.NewDefaultConfig(), | ||||
| 		Handlers: HandlerConfigs{ | ||||
| 			Processing: processinghandler.NewDefaultConfig(), | ||||
| 			Stream:     streamhandler.NewDefaultConfig(), | ||||
| @@ -86,6 +89,10 @@ func LoadConfigFromEnv(c *Config) (*Config, error) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if _, err = clientfeatures.LoadConfigFromEnv(&c.ClientFeatures); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if _, err = processinghandler.LoadConfigFromEnv(&c.Handlers.Processing); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import ( | ||||
| 	"net/url" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/auximageprovider" | ||||
| 	"github.com/imgproxy/imgproxy/v3/clientfeatures" | ||||
| 	"github.com/imgproxy/imgproxy/v3/cookies" | ||||
| 	"github.com/imgproxy/imgproxy/v3/errorreport" | ||||
| 	"github.com/imgproxy/imgproxy/v3/handlers" | ||||
| @@ -25,6 +26,7 @@ import ( | ||||
| // HandlerContext provides access to shared handler dependencies | ||||
| type HandlerContext interface { | ||||
| 	Workers() *workers.Workers | ||||
| 	ClientFeaturesDetector() *clientfeatures.Detector | ||||
| 	FallbackImage() auximageprovider.Provider | ||||
| 	ImageDataFactory() *imagedata.Factory | ||||
| 	Security() *security.Checker | ||||
| @@ -116,7 +118,8 @@ func (h *Handler) newRequest( | ||||
| 	} | ||||
|  | ||||
| 	// parse image url and processing options | ||||
| 	o, imageURL, err := h.OptionsParser().ParsePath(path, req.Header) | ||||
| 	features := h.ClientFeaturesDetector().Features(req.Header) | ||||
| 	o, imageURL, err := h.OptionsParser().ParsePath(path, &features) | ||||
| 	if err != nil { | ||||
| 		return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryPathParsing)) | ||||
| 	} | ||||
|   | ||||
| @@ -183,7 +183,6 @@ func (r *request) writeDebugHeaders(result *processing.Result, originData imaged | ||||
| // respondWithNotModified writes not-modified response | ||||
| func (r *request) respondWithNotModified() error { | ||||
| 	r.rw.SetExpires(r.opts.GetTime(keys.Expires)) | ||||
| 	r.rw.SetVary() | ||||
|  | ||||
| 	if r.config.LastModifiedEnabled { | ||||
| 		r.rw.Passthrough(httpheaders.LastModified) | ||||
| @@ -193,6 +192,8 @@ func (r *request) respondWithNotModified() error { | ||||
| 		r.rw.Passthrough(httpheaders.Etag) | ||||
| 	} | ||||
|  | ||||
| 	r.ClientFeaturesDetector().SetVary(r.rw.Header()) | ||||
|  | ||||
| 	r.rw.WriteHeader(http.StatusNotModified) | ||||
|  | ||||
| 	server.LogResponse( | ||||
| @@ -223,9 +224,10 @@ func (r *request) respondWithImage(statusCode int, resultData imagedata.ImageDat | ||||
| 		r.opts.GetBool(keys.ReturnAttachment, false), | ||||
| 	) | ||||
| 	r.rw.SetExpires(r.opts.GetTime(keys.Expires)) | ||||
| 	r.rw.SetVary() | ||||
| 	r.rw.SetCanonical(r.imageURL) | ||||
|  | ||||
| 	r.ClientFeaturesDetector().SetVary(r.rw.Header()) | ||||
|  | ||||
| 	if r.config.LastModifiedEnabled { | ||||
| 		r.rw.Passthrough(httpheaders.LastModified) | ||||
| 	} | ||||
|   | ||||
| @@ -30,6 +30,7 @@ const ( | ||||
| 	Cookie                          = "Cookie" | ||||
| 	Date                            = "Date" | ||||
| 	Dnt                             = "Dnt" | ||||
| 	Dpr                             = "Dpr" | ||||
| 	Etag                            = "Etag" | ||||
| 	Expect                          = "Expect" | ||||
| 	ExpectCt                        = "Expect-Ct" | ||||
| @@ -50,6 +51,8 @@ const ( | ||||
| 	Referer                         = "Referer" | ||||
| 	RequestId                       = "Request-Id" | ||||
| 	RetryAfter                      = "Retry-After" | ||||
| 	SecChDpr                        = "Sec-Ch-Dpr" | ||||
| 	SecChWidth                      = "Sec-Ch-Width" | ||||
| 	Server                          = "Server" | ||||
| 	SetCookie                       = "Set-Cookie" | ||||
| 	StrictTransportSecurity         = "Strict-Transport-Security" | ||||
| @@ -57,6 +60,7 @@ const ( | ||||
| 	UserAgent                       = "User-Agent" | ||||
| 	Vary                            = "Vary" | ||||
| 	Via                             = "Via" | ||||
| 	Width                           = "Width" | ||||
| 	WwwAuthenticate                 = "Www-Authenticate" | ||||
| 	XAmznRequestContextHeader       = "x-amzn-request-context" | ||||
| 	XContentTypeOptions             = "X-Content-Type-Options" | ||||
|   | ||||
							
								
								
									
										59
									
								
								imgproxy.go
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								imgproxy.go
									
									
									
									
									
								
							| @@ -6,6 +6,7 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/auximageprovider" | ||||
| 	"github.com/imgproxy/imgproxy/v3/clientfeatures" | ||||
| 	"github.com/imgproxy/imgproxy/v3/cookies" | ||||
| 	"github.com/imgproxy/imgproxy/v3/errorreport" | ||||
| 	"github.com/imgproxy/imgproxy/v3/fetcher" | ||||
| @@ -38,19 +39,20 @@ type ImgproxyHandlers struct { | ||||
|  | ||||
| // Imgproxy holds all the components needed for imgproxy to function | ||||
| type Imgproxy struct { | ||||
| 	workers          *workers.Workers | ||||
| 	fallbackImage    auximageprovider.Provider | ||||
| 	watermarkImage   auximageprovider.Provider | ||||
| 	fetcher          *fetcher.Fetcher | ||||
| 	imageDataFactory *imagedata.Factory | ||||
| 	handlers         ImgproxyHandlers | ||||
| 	security         *security.Checker | ||||
| 	optionsParser    *optionsparser.Parser | ||||
| 	processor        *processing.Processor | ||||
| 	cookies          *cookies.Cookies | ||||
| 	monitoring       *monitoring.Monitoring | ||||
| 	config           *Config | ||||
| 	errorReporter    *errorreport.Reporter | ||||
| 	workers                *workers.Workers | ||||
| 	fallbackImage          auximageprovider.Provider | ||||
| 	watermarkImage         auximageprovider.Provider | ||||
| 	fetcher                *fetcher.Fetcher | ||||
| 	imageDataFactory       *imagedata.Factory | ||||
| 	clientFeaturesDetector *clientfeatures.Detector | ||||
| 	handlers               ImgproxyHandlers | ||||
| 	security               *security.Checker | ||||
| 	optionsParser          *optionsparser.Parser | ||||
| 	processor              *processing.Processor | ||||
| 	cookies                *cookies.Cookies | ||||
| 	monitoring             *monitoring.Monitoring | ||||
| 	config                 *Config | ||||
| 	errorReporter          *errorreport.Reporter | ||||
| } | ||||
|  | ||||
| // New creates a new imgproxy instance | ||||
| @@ -66,6 +68,8 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) { | ||||
|  | ||||
| 	idf := imagedata.NewFactory(fetcher) | ||||
|  | ||||
| 	clientFeaturesDetector := clientfeatures.NewDetector(&config.ClientFeatures) | ||||
|  | ||||
| 	fallbackImage, err := auximageprovider.NewStaticProvider(ctx, &config.FallbackImage, "fallback", idf) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @@ -112,18 +116,19 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) { | ||||
| 	} | ||||
|  | ||||
| 	imgproxy := &Imgproxy{ | ||||
| 		workers:          workers, | ||||
| 		fallbackImage:    fallbackImage, | ||||
| 		watermarkImage:   watermarkImage, | ||||
| 		fetcher:          fetcher, | ||||
| 		imageDataFactory: idf, | ||||
| 		config:           config, | ||||
| 		security:         security, | ||||
| 		optionsParser:    optionsParser, | ||||
| 		processor:        processor, | ||||
| 		cookies:          cookies, | ||||
| 		monitoring:       monitoring, | ||||
| 		errorReporter:    errorReporter, | ||||
| 		workers:                workers, | ||||
| 		fallbackImage:          fallbackImage, | ||||
| 		watermarkImage:         watermarkImage, | ||||
| 		fetcher:                fetcher, | ||||
| 		imageDataFactory:       idf, | ||||
| 		clientFeaturesDetector: clientFeaturesDetector, | ||||
| 		config:                 config, | ||||
| 		security:               security, | ||||
| 		optionsParser:          optionsParser, | ||||
| 		processor:              processor, | ||||
| 		cookies:                cookies, | ||||
| 		monitoring:             monitoring, | ||||
| 		errorReporter:          errorReporter, | ||||
| 	} | ||||
|  | ||||
| 	imgproxy.handlers.Health = healthhandler.New() | ||||
| @@ -249,6 +254,10 @@ func (i *Imgproxy) ImageDataFactory() *imagedata.Factory { | ||||
| 	return i.imageDataFactory | ||||
| } | ||||
|  | ||||
| func (i *Imgproxy) ClientFeaturesDetector() *clientfeatures.Detector { | ||||
| 	return i.clientFeaturesDetector | ||||
| } | ||||
|  | ||||
| func (i *Imgproxy) Security() *security.Checker { | ||||
| 	return i.security | ||||
| } | ||||
|   | ||||
| @@ -477,9 +477,9 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvg() { | ||||
| 	s.Require().Equal("image/png", res.Header.Get(httpheaders.ContentType)) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() { | ||||
| func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceWebP() { | ||||
| 	s.Config().Processing.AlwaysRasterizeSvg = true | ||||
| 	s.Config().OptionsParser.EnforceWebp = true | ||||
| 	s.Config().ClientFeatures.EnforceWebp = true | ||||
|  | ||||
| 	res := s.GET("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}}) | ||||
|  | ||||
| @@ -489,7 +489,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() { | ||||
|  | ||||
| func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() { | ||||
| 	s.Config().Processing.AlwaysRasterizeSvg = false | ||||
| 	s.Config().OptionsParser.EnforceWebp = true | ||||
| 	s.Config().ClientFeatures.EnforceWebp = true | ||||
|  | ||||
| 	res := s.GET("/unsafe/plain/local:///test1.svg") | ||||
|  | ||||
|   | ||||
| @@ -20,13 +20,6 @@ var ( | ||||
| 	IMGPROXY_ONLY_PRESETS                 = env.Describe("IMGPROXY_ONLY_PRESETS", "boolean") | ||||
| 	IMGPROXY_ALLOWED_PROCESSING_OPTIONS   = env.Describe("IMGPROXY_ALLOWED_PROCESSING_OPTIONS", "comma-separated list of strings") | ||||
| 	IMGPROXY_ALLOW_SECURITY_OPTIONS       = env.Describe("IMGPROXY_ALLOW_SECURITY_OPTIONS", "boolean") | ||||
| 	IMGPROXY_AUTO_WEBP                    = env.Describe("IMGPROXY_AUTO_WEBP", "boolean") | ||||
| 	IMGPROXY_ENFORCE_WEBP                 = env.Describe("IMGPROXY_ENFORCE_WEBP", "boolean") | ||||
| 	IMGPROXY_AUTO_AVIF                    = env.Describe("IMGPROXY_AUTO_AVIF", "boolean") | ||||
| 	IMGPROXY_ENFORCE_AVIF                 = env.Describe("IMGPROXY_ENFORCE_AVIF", "boolean") | ||||
| 	IMGPROXY_AUTO_JXL                     = env.Describe("IMGPROXY_AUTO_JXL", "boolean") | ||||
| 	IMGPROXY_ENFORCE_JXL                  = env.Describe("IMGPROXY_ENFORCE_JXL", "boolean") | ||||
| 	IMGPROXY_ENABLE_CLIENT_HINTS          = env.Describe("IMGPROXY_ENABLE_CLIENT_HINTS", "boolean") | ||||
| 	IMGPROXY_ARGUMENTS_SEPARATOR          = env.Describe("IMGPROXY_ARGUMENTS_SEPARATOR", "string") | ||||
| 	IMGPROXY_BASE_URL                     = env.Describe("IMGPROXY_BASE_URL", "string") | ||||
| 	IMGPROXY_URL_REPLACEMENTS             = env.Describe("IMGPROXY_URL_REPLACEMENTS", "comma-separated list of key=value pairs") | ||||
| @@ -46,17 +39,6 @@ type Config struct { | ||||
| 	AllowedProcessingOptions []string // List of allowed processing options | ||||
| 	AllowSecurityOptions     bool     // Whether to allow security options in URLs | ||||
|  | ||||
| 	// 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 | ||||
| @@ -73,17 +55,6 @@ func NewDefaultConfig() Config { | ||||
| 		// Security and validation | ||||
| 		AllowSecurityOptions: false, | ||||
|  | ||||
| 		// 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:                   "", | ||||
| @@ -120,17 +91,6 @@ func LoadConfigFromEnv(c *Config) (*Config, error) { | ||||
| 		env.StringSlice(&c.AllowedProcessingOptions, IMGPROXY_ALLOWED_PROCESSING_OPTIONS), | ||||
| 		env.Bool(&c.AllowSecurityOptions, IMGPROXY_ALLOW_SECURITY_OPTIONS), | ||||
|  | ||||
| 		// Format preference and enforcement | ||||
| 		env.Bool(&c.AutoWebp, IMGPROXY_AUTO_WEBP), | ||||
| 		env.Bool(&c.EnforceWebp, IMGPROXY_ENFORCE_WEBP), | ||||
| 		env.Bool(&c.AutoAvif, IMGPROXY_AUTO_AVIF), | ||||
| 		env.Bool(&c.EnforceAvif, IMGPROXY_ENFORCE_AVIF), | ||||
| 		env.Bool(&c.AutoJxl, IMGPROXY_AUTO_JXL), | ||||
| 		env.Bool(&c.EnforceJxl, IMGPROXY_ENFORCE_JXL), | ||||
|  | ||||
| 		// Client hints | ||||
| 		env.Bool(&c.EnableClientHints, IMGPROXY_ENABLE_CLIENT_HINTS), | ||||
|  | ||||
| 		// URL processing | ||||
| 		env.String(&c.ArgumentsSeparator, IMGPROXY_ARGUMENTS_SEPARATOR), | ||||
| 		env.String(&c.BaseURL, IMGPROXY_BASE_URL), | ||||
|   | ||||
| @@ -1,11 +1,10 @@ | ||||
| package optionsparser | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"slices" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/clientfeatures" | ||||
| 	"github.com/imgproxy/imgproxy/v3/ierrors" | ||||
| 	"github.com/imgproxy/imgproxy/v3/imath" | ||||
| 	"github.com/imgproxy/imgproxy/v3/options" | ||||
| @@ -13,8 +12,6 @@ import ( | ||||
| 	"github.com/imgproxy/imgproxy/v3/processing" | ||||
| ) | ||||
|  | ||||
| const maxClientHintDPR = 8 | ||||
|  | ||||
| func (p *Parser) applyURLOption( | ||||
| 	o *options.Options, | ||||
| 	name string, | ||||
| @@ -138,57 +135,45 @@ func (p *Parser) applyURLOptions( | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (p *Parser) defaultProcessingOptions(headers http.Header) (*options.Options, error) { | ||||
| func (p *Parser) defaultProcessingOptions( | ||||
| 	features *clientfeatures.Features, | ||||
| ) (*options.Options, error) { | ||||
| 	o := options.New() | ||||
|  | ||||
| 	headerAccept := headers.Get("Accept") | ||||
| 	if features != nil { | ||||
| 		if features.PreferWebP || features.EnforceWebP { | ||||
| 			o.Set(keys.PreferWebP, true) | ||||
| 		} | ||||
|  | ||||
| 	if (p.config.AutoWebp || p.config.EnforceWebp) && strings.Contains(headerAccept, "image/webp") { | ||||
| 		o.Set(keys.PreferWebP, true) | ||||
|  | ||||
| 		if p.config.EnforceWebp { | ||||
| 		if features.EnforceWebP { | ||||
| 			o.Set(keys.EnforceWebP, true) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if (p.config.AutoAvif || p.config.EnforceAvif) && strings.Contains(headerAccept, "image/avif") { | ||||
| 		o.Set(keys.PreferAvif, true) | ||||
| 		if features.PreferAvif || features.EnforceAvif { | ||||
| 			o.Set(keys.PreferAvif, true) | ||||
| 		} | ||||
|  | ||||
| 		if p.config.EnforceAvif { | ||||
| 		if features.EnforceAvif { | ||||
| 			o.Set(keys.EnforceAvif, true) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if (p.config.AutoJxl || p.config.EnforceJxl) && strings.Contains(headerAccept, "image/jxl") { | ||||
| 		o.Set(keys.PreferJxl, true) | ||||
| 		if features.PreferJxl || features.EnforceJxl { | ||||
| 			o.Set(keys.PreferJxl, true) | ||||
| 		} | ||||
|  | ||||
| 		if p.config.EnforceJxl { | ||||
| 		if features.EnforceJxl { | ||||
| 			o.Set(keys.EnforceJxl, true) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if p.config.EnableClientHints { | ||||
| 		dpr := 1.0 | ||||
|  | ||||
| 		headerDPR := headers.Get("Sec-CH-DPR") | ||||
| 		if len(headerDPR) == 0 { | ||||
| 			headerDPR = headers.Get("DPR") | ||||
| 		} | ||||
| 		if len(headerDPR) > 0 { | ||||
| 			if d, err := strconv.ParseFloat(headerDPR, 64); err == nil && (d > 0 && d <= maxClientHintDPR) { | ||||
| 				dpr = d | ||||
| 				o.Set(keys.Dpr, dpr) | ||||
| 			} | ||||
| 		if features.ClientHintsDPR > 0 { | ||||
| 			o.Set(keys.Dpr, features.ClientHintsDPR) | ||||
| 			dpr = features.ClientHintsDPR | ||||
| 		} | ||||
|  | ||||
| 		headerWidth := headers.Get("Sec-CH-Width") | ||||
| 		if len(headerWidth) == 0 { | ||||
| 			headerWidth = headers.Get("Width") | ||||
| 		} | ||||
| 		if len(headerWidth) > 0 { | ||||
| 			if w, err := strconv.Atoi(headerWidth); err == nil { | ||||
| 				o.Set(keys.Width, imath.Shrink(w, dpr)) | ||||
| 			} | ||||
| 		if features.ClientHintsWidth > 0 { | ||||
| 			o.Set(keys.Width, imath.Shrink(features.ClientHintsWidth, dpr)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -204,7 +189,7 @@ func (p *Parser) defaultProcessingOptions(headers http.Header) (*options.Options | ||||
| // ParsePath parses the given request path and returns the processing options and image URL | ||||
| func (p *Parser) ParsePath( | ||||
| 	path string, | ||||
| 	headers http.Header, | ||||
| 	features *clientfeatures.Features, | ||||
| ) (o *options.Options, imageURL string, err error) { | ||||
| 	if path == "" || path == "/" { | ||||
| 		return nil, "", newInvalidURLError("invalid path: %s", path) | ||||
| @@ -213,9 +198,9 @@ func (p *Parser) ParsePath( | ||||
| 	parts := strings.Split(strings.TrimPrefix(path, "/"), "/") | ||||
|  | ||||
| 	if p.config.OnlyPresets { | ||||
| 		o, imageURL, err = p.parsePathPresets(parts, headers) | ||||
| 		o, imageURL, err = p.parsePathPresets(parts, features) | ||||
| 	} else { | ||||
| 		o, imageURL, err = p.parsePathOptions(parts, headers) | ||||
| 		o, imageURL, err = p.parsePathOptions(parts, features) | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -228,13 +213,13 @@ func (p *Parser) ParsePath( | ||||
| // parsePathOptions parses processing options from the URL path | ||||
| func (p *Parser) parsePathOptions( | ||||
| 	parts []string, | ||||
| 	headers http.Header, | ||||
| 	features *clientfeatures.Features, | ||||
| ) (*options.Options, string, error) { | ||||
| 	if _, ok := processing.ResizeTypes[parts[0]]; ok { | ||||
| 		return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format") | ||||
| 	} | ||||
|  | ||||
| 	o, err := p.defaultProcessingOptions(headers) | ||||
| 	o, err := p.defaultProcessingOptions(features) | ||||
| 	if err != nil { | ||||
| 		return nil, "", err | ||||
| 	} | ||||
| @@ -260,8 +245,11 @@ func (p *Parser) parsePathOptions( | ||||
| } | ||||
|  | ||||
| // parsePathPresets parses presets from the URL path | ||||
| func (p *Parser) parsePathPresets(parts []string, headers http.Header) (*options.Options, string, error) { | ||||
| 	o, err := p.defaultProcessingOptions(headers) | ||||
| func (p *Parser) parsePathPresets( | ||||
| 	parts []string, | ||||
| 	features *clientfeatures.Features, | ||||
| ) (*options.Options, string, error) { | ||||
| 	o, err := p.defaultProcessingOptions(features) | ||||
| 	if err != nil { | ||||
| 		return nil, "", err | ||||
| 	} | ||||
|   | ||||
| @@ -3,13 +3,13 @@ package optionsparser | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/clientfeatures" | ||||
| 	"github.com/imgproxy/imgproxy/v3/imagetype" | ||||
| 	"github.com/imgproxy/imgproxy/v3/options" | ||||
| 	"github.com/imgproxy/imgproxy/v3/options/keys" | ||||
| @@ -50,7 +50,7 @@ func (s *ProcessingOptionsTestSuite) SetupSubTest() { | ||||
| 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))) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal(originURL, imageURL) | ||||
| @@ -62,7 +62,7 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithFilename() { | ||||
|  | ||||
| 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value" | ||||
| 	path := fmt.Sprintf("/size:100:100/%s.png/puppy.jpg", base64.RawURLEncoding.EncodeToString([]byte(originURL))) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal(originURL, imageURL) | ||||
| @@ -72,7 +72,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))) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal(originURL, imageURL) | ||||
| @@ -84,7 +84,7 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithBase() { | ||||
|  | ||||
| 	originURL := "lorem/ipsum.jpg?param=value" | ||||
| 	path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL))) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL) | ||||
| @@ -99,7 +99,7 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithReplacement() { | ||||
|  | ||||
| 	originURL := "test://lorem/ipsum.jpg?param=value" | ||||
| 	path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL))) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal("http://images.dev/lorem/dolor/ipsum.jpg?param=value", imageURL) | ||||
| @@ -109,7 +109,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) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal(originURL, imageURL) | ||||
| @@ -120,7 +120,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithoutExtension() { | ||||
| 	originURL := "http://images.dev/lorem/ipsum.jpg" | ||||
| 	path := fmt.Sprintf("/size:100:100/plain/%s", originURL) | ||||
|  | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal(originURL, imageURL) | ||||
| @@ -129,7 +129,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)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal(originURL, imageURL) | ||||
| @@ -141,7 +141,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithBase() { | ||||
|  | ||||
| 	originURL := "lorem/ipsum.jpg" | ||||
| 	path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL) | ||||
| @@ -156,7 +156,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithReplacement() { | ||||
|  | ||||
| 	originURL := "test://lorem/ipsum.jpg" | ||||
| 	path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal("http://images.dev/lorem/dolor/ipsum.jpg", imageURL) | ||||
| @@ -168,7 +168,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscapedWithBase() { | ||||
|  | ||||
| 	originURL := "lorem/ipsum.jpg?param=value" | ||||
| 	path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL) | ||||
| @@ -179,7 +179,7 @@ func (s *ProcessingOptionsTestSuite) TestParseWithArgumentsSeparator() { | ||||
| 	s.config().ArgumentsSeparator = "," | ||||
|  | ||||
| 	path := "/size,100,100,1/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -190,7 +190,7 @@ func (s *ProcessingOptionsTestSuite) TestParseWithArgumentsSeparator() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathFormat() { | ||||
| 	path := "/format:webp/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -199,7 +199,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathFormat() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathResize() { | ||||
| 	path := "/resize:fill:100:200:1/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -211,7 +211,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathResize() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() { | ||||
| 	path := "/resizing_type:fill/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -220,7 +220,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathSize() { | ||||
| 	path := "/size:100:200:1/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -231,7 +231,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathSize() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathWidth() { | ||||
| 	path := "/width:100/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -240,7 +240,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidth() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathHeight() { | ||||
| 	path := "/height:100/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -249,7 +249,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathHeight() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() { | ||||
| 	path := "/enlarge:1/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -258,7 +258,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathExtend() { | ||||
| 	path := "/extend:1:so:10:20/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -273,21 +273,21 @@ func (s *ProcessingOptionsTestSuite) TestParsePathExtend() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathExtendSmartGravity() { | ||||
| 	path := "/extend:1:sm/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	_, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	_, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().Error(err) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathExtendReplicateGravity() { | ||||
| 	path := "/extend:1:re/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	_, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	_, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().Error(err) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathGravity() { | ||||
| 	path := "/gravity:soea/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -299,7 +299,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathGravity() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocusPoint() { | ||||
| 	path := "/gravity:fp:0.5:0.75/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -310,14 +310,14 @@ func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocusPoint() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathGravityReplicate() { | ||||
| 	path := "/gravity:re/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	_, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	_, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().Error(err) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathCrop() { | ||||
| 	path := "/crop:100:200/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -333,7 +333,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCrop() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathCropGravity() { | ||||
| 	path := "/crop:100:200:nowe:10:20/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -349,14 +349,14 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCropGravity() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathCropGravityReplicate() { | ||||
| 	path := "/crop:100:200:re/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	_, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	_, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().Error(err) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathQuality() { | ||||
| 	path := "/quality:55/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -365,7 +365,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathQuality() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathBackground() { | ||||
| 	path := "/background:128:129:130/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -377,7 +377,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackground() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() { | ||||
| 	path := "/background:ffddee/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -389,7 +389,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() { | ||||
| 	path := "/background:fff/background:/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -398,7 +398,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathBlur() { | ||||
| 	path := "/blur:0.2/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -407,7 +407,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBlur() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() { | ||||
| 	path := "/sharpen:0.2/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -416,7 +416,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathDpr() { | ||||
| 	path := "/dpr:2/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -425,7 +425,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" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -446,7 +446,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPreset() { | ||||
| 	} | ||||
|  | ||||
| 	path := "/preset:test1:test2/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -462,7 +462,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() { | ||||
| 	} | ||||
|  | ||||
| 	path := "/quality:70/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -479,7 +479,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() { | ||||
| 	} | ||||
|  | ||||
| 	path := "/preset:test1/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -488,7 +488,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() { | ||||
| 	path := "/cachebuster:123/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -497,7 +497,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() { | ||||
| 	path := "/strip_metadata:true/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -505,159 +505,159 @@ func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() { | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() { | ||||
| 	s.config().AutoWebp = true | ||||
|  | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	headers := http.Header{"Accept": []string{"image/webp"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
| 	features := clientfeatures.Features{PreferWebP: true} | ||||
| 	o, _, err := s.parser().ParsePath(path, &features) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().True(o.GetBool(keys.PreferWebP, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceWebP, false)) | ||||
| 	s.Require().False(o.GetBool(keys.PreferAvif, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceAvif, false)) | ||||
| 	s.Require().False(o.GetBool(keys.PreferJxl, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceJxl, false)) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() { | ||||
| 	s.config().EnforceWebp = true | ||||
|  | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png" | ||||
| 	headers := http.Header{"Accept": []string{"image/webp"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
| 	features := clientfeatures.Features{EnforceWebP: true} | ||||
| 	o, _, err := s.parser().ParsePath(path, &features) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().True(o.GetBool(keys.PreferWebP, false)) | ||||
| 	s.Require().True(o.GetBool(keys.EnforceWebP, false)) | ||||
| 	s.Require().False(o.GetBool(keys.PreferAvif, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceAvif, false)) | ||||
| 	s.Require().False(o.GetBool(keys.PreferJxl, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceJxl, false)) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathAvifDetection() { | ||||
| 	s.config().AutoAvif = true | ||||
|  | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	headers := http.Header{"Accept": []string{"image/avif"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
| 	features := clientfeatures.Features{PreferAvif: true} | ||||
| 	o, _, err := s.parser().ParsePath(path, &features) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().False(o.GetBool(keys.PreferWebP, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceWebP, false)) | ||||
| 	s.Require().True(o.GetBool(keys.PreferAvif, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceAvif, false)) | ||||
| 	s.Require().False(o.GetBool(keys.PreferJxl, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceJxl, false)) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathAvifEnforce() { | ||||
| 	s.config().EnforceAvif = true | ||||
|  | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png" | ||||
| 	headers := http.Header{"Accept": []string{"image/avif"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
| 	features := clientfeatures.Features{EnforceAvif: true} | ||||
| 	o, _, err := s.parser().ParsePath(path, &features) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().False(o.GetBool(keys.PreferWebP, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceWebP, false)) | ||||
| 	s.Require().True(o.GetBool(keys.PreferAvif, false)) | ||||
| 	s.Require().True(o.GetBool(keys.EnforceAvif, false)) | ||||
| 	s.Require().False(o.GetBool(keys.PreferJxl, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceJxl, false)) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathJxlDetection() { | ||||
| 	s.config().AutoJxl = true | ||||
|  | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	headers := http.Header{"Accept": []string{"image/jxl"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
| 	features := clientfeatures.Features{PreferJxl: true} | ||||
| 	o, _, err := s.parser().ParsePath(path, &features) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().False(o.GetBool(keys.PreferWebP, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceWebP, false)) | ||||
| 	s.Require().False(o.GetBool(keys.PreferAvif, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceAvif, false)) | ||||
| 	s.Require().True(o.GetBool(keys.PreferJxl, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceJxl, false)) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathJxlEnforce() { | ||||
| 	s.config().EnforceJxl = true | ||||
|  | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png" | ||||
| 	headers := http.Header{"Accept": []string{"image/jxl"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
| 	features := clientfeatures.Features{EnforceJxl: true} | ||||
| 	o, _, err := s.parser().ParsePath(path, &features) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().False(o.GetBool(keys.PreferWebP, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceWebP, false)) | ||||
| 	s.Require().False(o.GetBool(keys.PreferAvif, false)) | ||||
| 	s.Require().False(o.GetBool(keys.EnforceAvif, false)) | ||||
| 	s.Require().True(o.GetBool(keys.PreferJxl, false)) | ||||
| 	s.Require().True(o.GetBool(keys.EnforceJxl, false)) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() { | ||||
| 	s.config().EnableClientHints = true | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathClientHints() { | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png" | ||||
| 	headers := http.Header{"Width": []string{"100"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	testCases := []struct { | ||||
| 		name     string | ||||
| 		features clientfeatures.Features | ||||
| 		width    int | ||||
| 		dpr      float64 | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "NoClientHints", | ||||
| 			features: clientfeatures.Features{}, | ||||
| 			width:    0, | ||||
| 			dpr:      1.0, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "WidthOnly", | ||||
| 			features: clientfeatures.Features{ClientHintsWidth: 100}, | ||||
| 			width:    100, | ||||
| 			dpr:      1.0, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "DprOnly", | ||||
| 			features: clientfeatures.Features{ClientHintsDPR: 2.0}, | ||||
| 			width:    0, | ||||
| 			dpr:      2.0, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "WidthAndDpr", | ||||
| 			features: clientfeatures.Features{ClientHintsWidth: 100, ClientHintsDPR: 2.0}, | ||||
| 			width:    50, | ||||
| 			dpr:      2.0, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	s.Require().Equal(100, o.GetInt(keys.Width, 0)) | ||||
| 	for _, tc := range testCases { | ||||
| 		s.Run(tc.name, func() { | ||||
| 			o, _, err := s.parser().ParsePath(path, &tc.features) | ||||
|  | ||||
| 			s.Require().NoError(err) | ||||
| 			s.Require().Equal(tc.width, o.GetInt(keys.Width, 0)) | ||||
| 			s.Require().InDelta(tc.dpr, o.GetFloat(keys.Dpr, 1.0), 0.0001) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() { | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png" | ||||
| 	headers := http.Header{"Width": []string{"100"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().Equal(0, o.GetInt(keys.Width, 0)) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() { | ||||
| 	s.config().EnableClientHints = true | ||||
|  | ||||
| 	path := "/width:150/plain/http://images.dev/lorem/ipsum.jpg@png" | ||||
| 	headers := http.Header{"Width": []string{"100"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathClientHintsRedefine() { | ||||
| 	path := "/width:150/dpr:3.0/plain/http://images.dev/lorem/ipsum.jpg@png" | ||||
| 	features := clientfeatures.Features{ | ||||
| 		ClientHintsWidth: 100, | ||||
| 		ClientHintsDPR:   2.0, | ||||
| 	} | ||||
| 	o, _, err := s.parser().ParsePath(path, &features) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().Equal(150, o.GetInt(keys.Width, 0)) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() { | ||||
| 	s.config().EnableClientHints = true | ||||
|  | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png" | ||||
| 	headers := http.Header{"Dpr": []string{"2"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().InDelta(2.0, o.GetFloat(keys.Dpr, 1.0), 0.0001) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() { | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png" | ||||
| 	headers := http.Header{"Dpr": []string{"2"}} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().InDelta(1.0, o.GetFloat(keys.Dpr, 1.0), 0.0001) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParsePathWidthAndDprHeaderCombined() { | ||||
| 	s.config().EnableClientHints = true | ||||
|  | ||||
| 	path := "/plain/http://images.dev/lorem/ipsum.jpg@png" | ||||
| 	headers := http.Header{ | ||||
| 		"Width": []string{"100"}, | ||||
| 		"Dpr":   []string{"2"}, | ||||
| 	} | ||||
| 	o, _, err := s.parser().ParsePath(path, headers) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| 	s.Require().Equal(50, o.GetInt(keys.Width, 0)) | ||||
| 	s.Require().InDelta(2.0, o.GetFloat(keys.Dpr, 1.0), 0.0001) | ||||
| 	s.Require().InDelta(3.0, o.GetFloat(keys.Dpr, 1.0), 0.0001) | ||||
| } | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() { | ||||
| 	path := "/skp:jpg:png/plain/http://images.dev/lorem/ipsum.jpg" | ||||
|  | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -670,7 +670,7 @@ func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() { | ||||
| func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() { | ||||
| 	path := "/skp:jpg:png:bad_format/plain/http://images.dev/lorem/ipsum.jpg" | ||||
|  | ||||
| 	_, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	_, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().Error(err) | ||||
| 	s.Require().Equal("Invalid image format in skip_processing: bad_format", err.Error()) | ||||
| @@ -678,7 +678,7 @@ func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParseExpires() { | ||||
| 	path := "/exp:32503669200/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	o, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
| 	s.Require().Equal(time.Unix(32503669200, 0), o.GetTime(keys.Expires)) | ||||
| @@ -686,7 +686,7 @@ func (s *ProcessingOptionsTestSuite) TestParseExpires() { | ||||
|  | ||||
| func (s *ProcessingOptionsTestSuite) TestParseExpiresExpired() { | ||||
| 	path := "/exp:1609448400/plain/http://images.dev/lorem/ipsum.jpg" | ||||
| 	_, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	_, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().Error(err, "Expired URL") | ||||
| } | ||||
| @@ -701,7 +701,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() { | ||||
| 	originURL := "http://images.dev/lorem/ipsum.jpg" | ||||
| 	path := "/test1:test2/plain/" + originURL + "@png" | ||||
|  | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -721,7 +721,7 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() { | ||||
| 	originURL := "http://images.dev/lorem/ipsum.jpg?param=value" | ||||
| 	path := fmt.Sprintf("/test1:test2/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL))) | ||||
|  | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 	o, imageURL, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 	s.Require().NoError(err) | ||||
|  | ||||
| @@ -752,7 +752,7 @@ func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() { | ||||
| 			} | ||||
|  | ||||
| 			path := fmt.Sprintf("/%s/%s.png", tc.options, base64.RawURLEncoding.EncodeToString([]byte(originURL))) | ||||
| 			_, _, err := s.parser().ParsePath(path, make(http.Header)) | ||||
| 			_, _, err := s.parser().ParsePath(path, nil) | ||||
|  | ||||
| 			if len(tc.expectedError) > 0 { | ||||
| 				s.Require().Error(err) | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package responsewriter | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/ensure" | ||||
| @@ -15,16 +14,6 @@ var ( | ||||
| 	IMGPROXY_FALLBACK_IMAGE_TTL        = env.Describe("IMGPROXY_FALLBACK_IMAGE_TTL", "seconds >= 0") | ||||
| 	IMGPROXY_CACHE_CONTROL_PASSTHROUGH = env.Describe("IMGPROXY_CACHE_CONTROL_PASSTHROUGH", "boolean") | ||||
| 	IMGPROXY_WRITE_RESPONSE_TIMEOUT    = env.Describe("IMGPROXY_WRITE_RESPONSE_TIMEOUT", "seconds > 0") | ||||
|  | ||||
| 	// NOTE: These are referenced here to determine if we need to set the Vary header | ||||
| 	// Unfotunately, we can not reuse them from optionsparser package due to import cycle | ||||
| 	IMGPROXY_AUTO_WEBP           = env.Describe("IMGPROXY_AUTO_WEBP", "boolean") | ||||
| 	IMGPROXY_AUTO_AVIF           = env.Describe("IMGPROXY_AUTO_AVIF", "boolean") | ||||
| 	IMGPROXY_AUTO_JXL            = env.Describe("IMGPROXY_AUTO_JXL", "boolean") | ||||
| 	IMGPROXY_ENFORCE_WEBP        = env.Describe("IMGPROXY_ENFORCE_WEBP", "boolean") | ||||
| 	IMGPROXY_ENFORCE_AVIF        = env.Describe("IMGPROXY_ENFORCE_AVIF", "boolean") | ||||
| 	IMGPROXY_ENFORCE_JXL         = env.Describe("IMGPROXY_ENFORCE_JXL", "boolean") | ||||
| 	IMGPROXY_ENABLE_CLIENT_HINTS = env.Describe("IMGPROXY_ENABLE_CLIENT_HINTS", "boolean") | ||||
| ) | ||||
|  | ||||
| // Config holds configuration for response writer | ||||
| @@ -33,7 +22,6 @@ type Config struct { | ||||
| 	DefaultTTL              int           // Default Cache-Control max-age= value for cached images | ||||
| 	FallbackImageTTL        int           // TTL for images served as fallbacks | ||||
| 	CacheControlPassthrough bool          // Passthrough the Cache-Control from the original response | ||||
| 	VaryValue               string        // Value for Vary header | ||||
| 	WriteResponseTimeout    time.Duration // Timeout for response write operations | ||||
| } | ||||
|  | ||||
| @@ -44,7 +32,6 @@ func NewDefaultConfig() Config { | ||||
| 		DefaultTTL:              31_536_000, | ||||
| 		FallbackImageTTL:        0, | ||||
| 		CacheControlPassthrough: false, | ||||
| 		VaryValue:               "", | ||||
| 		WriteResponseTimeout:    10 * time.Second, | ||||
| 	} | ||||
| } | ||||
| @@ -60,63 +47,8 @@ func LoadConfigFromEnv(c *Config) (*Config, error) { | ||||
| 		env.Bool(&c.CacheControlPassthrough, IMGPROXY_CACHE_CONTROL_PASSTHROUGH), | ||||
| 		env.Duration(&c.WriteResponseTimeout, IMGPROXY_WRITE_RESPONSE_TIMEOUT), | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	vary := make([]string, 0) | ||||
|  | ||||
| 	var ok bool | ||||
|  | ||||
| 	if err, ok = c.envEnableFormatDetection(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if ok { | ||||
| 		vary = append(vary, "Accept") | ||||
| 	} | ||||
|  | ||||
| 	if err, ok = c.envEnableClientHints(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if ok { | ||||
| 		vary = append(vary, "Sec-CH-DPR", "DPR", "Sec-CH-Width", "Width") | ||||
| 	} | ||||
|  | ||||
| 	c.VaryValue = strings.Join(vary, ", ") | ||||
|  | ||||
| 	return c, nil | ||||
| } | ||||
|  | ||||
| // envEnableFormatDetection checks if any of the format detection options are enabled | ||||
| func (c *Config) envEnableFormatDetection() (error, bool) { | ||||
| 	var autoWebp, enforceWebp, autoAvif, enforceAvif, autoJxl, enforceJxl bool | ||||
|  | ||||
| 	// We won't need those variables in runtime, hence, we could | ||||
| 	// read them here once into local variables | ||||
| 	err := errors.Join( | ||||
| 		env.Bool(&autoWebp, IMGPROXY_AUTO_WEBP), | ||||
| 		env.Bool(&enforceWebp, IMGPROXY_ENFORCE_WEBP), | ||||
| 		env.Bool(&autoAvif, IMGPROXY_AUTO_AVIF), | ||||
| 		env.Bool(&enforceAvif, IMGPROXY_ENFORCE_AVIF), | ||||
| 		env.Bool(&autoJxl, IMGPROXY_AUTO_JXL), | ||||
| 		env.Bool(&enforceJxl, IMGPROXY_ENFORCE_JXL), | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return err, false | ||||
| 	} | ||||
|  | ||||
| 	return nil, autoWebp || | ||||
| 		enforceWebp || | ||||
| 		autoAvif || | ||||
| 		enforceAvif || | ||||
| 		autoJxl || | ||||
| 		enforceJxl | ||||
| } | ||||
|  | ||||
| // envEnableClientHints checks if client hints are enabled | ||||
| func (c *Config) envEnableClientHints() (err error, ok bool) { | ||||
| 	err = env.Bool(&ok, IMGPROXY_ENABLE_CLIENT_HINTS) | ||||
| 	return | ||||
| 	return c, err | ||||
| } | ||||
|  | ||||
| // Validate checks config for errors | ||||
|   | ||||
| @@ -1,113 +0,0 @@ | ||||
| package responsewriter | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/suite" | ||||
|  | ||||
| 	"github.com/imgproxy/imgproxy/v3/config" | ||||
| 	"github.com/imgproxy/imgproxy/v3/logger" | ||||
| ) | ||||
|  | ||||
| type ResponseWriterConfigSuite struct { | ||||
| 	suite.Suite | ||||
| } | ||||
|  | ||||
| func (s *ResponseWriterConfigSuite) SetupSuite() { | ||||
| 	logger.Mute() | ||||
| } | ||||
|  | ||||
| func (s *ResponseWriterConfigSuite) TearDownSuite() { | ||||
| 	logger.Unmute() | ||||
| } | ||||
|  | ||||
| func (s *ResponseWriterConfigSuite) TestLoadingVaryValueFromEnv() { | ||||
| 	defaultEnv := map[string]string{ | ||||
| 		"IMGPROXY_AUTO_WEBP":           "", | ||||
| 		"IMGPROXY_ENFORCE_WEBP":        "", | ||||
| 		"IMGPROXY_AUTO_AVIF":           "", | ||||
| 		"IMGPROXY_ENFORCE_AVIF":        "", | ||||
| 		"IMGPROXY_AUTO_JXL":            "", | ||||
| 		"IMGPROXY_ENFORCE_JXL":         "", | ||||
| 		"IMGPROXY_ENABLE_CLIENT_HINTS": "", | ||||
| 	} | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name     string | ||||
| 		env      map[string]string | ||||
| 		expected string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:     "AutoWebP", | ||||
| 			env:      map[string]string{"IMGPROXY_AUTO_WEBP": "true"}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "EnforceWebP", | ||||
| 			env:      map[string]string{"IMGPROXY_ENFORCE_WEBP": "true"}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "AutoAVIF", | ||||
| 			env:      map[string]string{"IMGPROXY_AUTO_AVIF": "true"}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "EnforceAVIF", | ||||
| 			env:      map[string]string{"IMGPROXY_ENFORCE_AVIF": "true"}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "AutoJXL", | ||||
| 			env:      map[string]string{"IMGPROXY_AUTO_JXL": "true"}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "EnforceJXL", | ||||
| 			env:      map[string]string{"IMGPROXY_ENFORCE_JXL": "true"}, | ||||
| 			expected: "Accept", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:     "EnableClientHints", | ||||
| 			env:      map[string]string{"IMGPROXY_ENABLE_CLIENT_HINTS": "true"}, | ||||
| 			expected: "Sec-CH-DPR, DPR, Sec-CH-Width, Width", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "Combined", | ||||
| 			env: map[string]string{ | ||||
| 				"IMGPROXY_AUTO_WEBP":           "true", | ||||
| 				"IMGPROXY_ENABLE_CLIENT_HINTS": "true", | ||||
| 			}, | ||||
| 			expected: "Accept, Sec-CH-DPR, DPR, Sec-CH-Width, Width", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		s.Run(fmt.Sprintf("%v", tc.env), func() { | ||||
| 			// Set default environment variables | ||||
| 			for key, value := range defaultEnv { | ||||
| 				s.T().Setenv(key, value) | ||||
| 			} | ||||
| 			// Set environment variables | ||||
| 			for key, value := range tc.env { | ||||
| 				s.T().Setenv(key, value) | ||||
| 			} | ||||
|  | ||||
| 			// TODO: Remove when we removed global config | ||||
| 			config.Reset() | ||||
| 			config.Configure() | ||||
|  | ||||
| 			// Load config | ||||
| 			cfg, err := LoadConfigFromEnv(nil) | ||||
|  | ||||
| 			// Assert expected values | ||||
| 			s.Require().NoError(err) | ||||
| 			s.Require().Equal(tc.expected, cfg.VaryValue) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestResponseWriterConfig(t *testing.T) { | ||||
| 	suite.Run(t, new(ResponseWriterConfigSuite)) | ||||
| } | ||||
| @@ -75,13 +75,6 @@ func (w *Writer) SetExpires(expires time.Time) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SetVary sets the Vary header | ||||
| func (w *Writer) SetVary() { | ||||
| 	if val := w.config.VaryValue; len(val) > 0 { | ||||
| 		w.result.Set(httpheaders.Vary, val) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SetContentDisposition sets the Content-Disposition header, passthrough to ContentDispositionValue | ||||
| func (w *Writer) SetContentDisposition(originURL, filename, ext, contentType string, returnAttachment bool) { | ||||
| 	value := httpheaders.ContentDispositionValue( | ||||
|   | ||||
| @@ -188,22 +188,6 @@ func (s *ResponseWriterSuite) TestHeaderCases() { | ||||
| 				w.SetExpires(shortExpires) | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "SetVaryHeader", | ||||
| 			req:  http.Header{}, | ||||
| 			res: http.Header{ | ||||
| 				httpheaders.Vary:                  []string{"Accept, Sec-CH-DPR, DPR, Sec-CH-Width, Width"}, | ||||
| 				httpheaders.CacheControl:          []string{"no-cache"}, | ||||
| 				httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"}, | ||||
| 			}, | ||||
| 			config: Config{ | ||||
| 				VaryValue:            "Accept, Sec-CH-DPR, DPR, Sec-CH-Width, Width", | ||||
| 				WriteResponseTimeout: writeResponseTimeout, | ||||
| 			}, | ||||
| 			fn: func(w *Writer) { | ||||
| 				w.SetVary() | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "PassthroughHeaders", | ||||
| 			req: http.Header{ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user