diff --git a/CHANGELOG.md b/CHANGELOG.md index daa0c20f..d3bbc195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] +### Add +- Add `IMGPROXY_URL_REPLACEMENTS` config. ## [3.17.0] - 2023-05-10 ### Add diff --git a/config/config.go b/config/config.go index b5438bce..b60c81e3 100644 --- a/config/config.go +++ b/config/config.go @@ -126,7 +126,8 @@ var ( LastModifiedEnabled bool - BaseURL string + BaseURL string + URLReplacements map[*regexp.Regexp]string Presets []string OnlyPresets bool @@ -317,6 +318,7 @@ func Reset() { LastModifiedEnabled = false BaseURL = "" + URLReplacements = make(map[*regexp.Regexp]string) Presets = make([]string, 0) OnlyPresets = false @@ -518,6 +520,9 @@ func Configure() error { configurators.Bool(&LastModifiedEnabled, "IMGPROXY_USE_LAST_MODIFIED") configurators.String(&BaseURL, "IMGPROXY_BASE_URL") + if err := configurators.Replacements(&URLReplacements, "IMGPROXY_URL_REPLACEMENTS"); err != nil { + return err + } configurators.StringSlice(&Presets, "IMGPROXY_PRESETS") if err := configurators.StringSliceFile(&Presets, presetsPath); err != nil { diff --git a/config/configurators/configurators.go b/config/configurators/configurators.go index 0079b6a4..945d21da 100644 --- a/config/configurators/configurators.go +++ b/config/configurators/configurators.go @@ -221,6 +221,26 @@ func Patterns(s *[]*regexp.Regexp, name string) { } } +func Replacements(m *map[*regexp.Regexp]string, name string) error { + var sm map[string]string + + if err := StringMap(&sm, name); err != nil { + return err + } + + if len(sm) > 0 { + mm := make(map[*regexp.Regexp]string) + + for k, v := range sm { + mm[RegexpFromPattern(k)] = v + } + + *m = mm + } + + return nil +} + func RegexpFromPattern(pattern string) *regexp.Regexp { var result strings.Builder // Perform prefix matching @@ -228,7 +248,7 @@ func RegexpFromPattern(pattern string) *regexp.Regexp { for i, part := range strings.Split(pattern, "*") { // Add a regexp match all without slashes for each wildcard character if i > 0 { - result.WriteString("[^/]*") + result.WriteString("([^/]*)") } // Quote other parts of the pattern diff --git a/docs/configuration.md b/docs/configuration.md index 4b4ae676..0d3bcb71 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -406,6 +406,16 @@ imgproxy can process files from OpenStack Object Storage, but this feature is di Check out the [Serving files from OpenStack Object Storage](serving_files_from_openstack_swift.md) guide to learn more. +## Source image URLs + +* `IMGPROXY_BASE_URL`: a base URL prefix that will be added to each source image URL. For example, if the base URL is `http://example.com/images` and `/path/to/image.png` is requested, imgproxy will download the source image from `http://example.com/images/path/to/image.png`. If the image URL already contains the prefix, it won't be added. Default: blank + +* `IMGPROXY_URL_REPLACEMENTS`: a list of `pattern=replacement` pairs, semicolon (`;`) divided. imgproxy will replace source URL prefixes matching the pattern with the corresponding replacement. Wildcards can be included in patterns with `*` to match all characters except `/`. `${N}` in replacement strings will be replaced with wildcard values, where `N` is the number of the wildcard. Examples: + * `mys3://=s3://my_bucket/images/` will replace `mys3://image01.jpg` with `s3://my_bucket/images/image01.jpg` + * `mys3://*/=s3://my_bucket/${1}/images` will replace `mys3://items/image01.jpg` with `s3://my_bucket/items/images/image01.jpg` + +**📝 Note:** Replacements defined in `IMGPROXY_URL_REPLACEMENTS` are applied before `IMGPROXY_BASE_URL` is added. + ## Metrics ### New Relic :id=new-relic-metrics @@ -527,7 +537,6 @@ imgproxy can send logs to syslog, but this feature is disabled by default. To en ## Miscellaneous -* `IMGPROXY_BASE_URL`: a base URL prefix that will be added to each requested image URL. For example, if the base URL is `http://example.com/images` and `/path/to/image.png` is requested, imgproxy will download the source image from `http://example.com/images/path/to/image.png`. If the image URL already contains the prefix, it won't be added. Default: blank * `IMGPROXY_USE_LINEAR_COLORSPACE`: when `true`, imgproxy will process images in linear colorspace. This will slow down processing. Note that images won't be fully processed in linear colorspace while shrink-on-load is enabled (see below). * `IMGPROXY_DISABLE_SHRINK_ON_LOAD`: when `true`, disables shrink-on-load for JPEGs and WebP files. Allows processing the entire image in linear colorspace but dramatically slows down resizing and increases memory usage when working with large images. * `IMGPROXY_STRIP_METADATA`: when `true`, imgproxy will strip all metadata (EXIF, IPTC, etc.) from JPEG and WebP output images. Default: `true` diff --git a/options/processing_options_test.go b/options/processing_options_test.go index 4727c041..2a48fa5a 100644 --- a/options/processing_options_test.go +++ b/options/processing_options_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "testing" "github.com/stretchr/testify/require" @@ -54,6 +55,20 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithBase() { require.Equal(s.T(), imagetype.PNG, po.Format) } +func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithReplacement() { + config.URLReplacements = map[*regexp.Regexp]string{ + regexp.MustCompile("^test://([^/]*)/"): "http://images.dev/${1}/dolor/", + } + + 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)) + + require.Nil(s.T(), err) + require.Equal(s.T(), "http://images.dev/lorem/dolor/ipsum.jpg?param=value", imageURL) + require.Equal(s.T(), imagetype.PNG, po.Format) +} + func (s *ProcessingOptionsTestSuite) TestParsePlainURL() { originURL := "http://images.dev/lorem/ipsum.jpg" path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL) @@ -96,6 +111,20 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithBase() { require.Equal(s.T(), imagetype.PNG, po.Format) } +func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithReplacement() { + config.URLReplacements = map[*regexp.Regexp]string{ + regexp.MustCompile("^test://([^/]*)/"): "http://images.dev/${1}/dolor/", + } + + originURL := "test://lorem/ipsum.jpg" + path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL) + po, imageURL, err := ParsePath(path, make(http.Header)) + + require.Nil(s.T(), err) + require.Equal(s.T(), "http://images.dev/lorem/dolor/ipsum.jpg", imageURL) + require.Equal(s.T(), imagetype.PNG, po.Format) +} + func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscapedWithBase() { config.BaseURL = "http://images.dev/" diff --git a/options/url.go b/options/url.go index 850b7967..b4e8e5cc 100644 --- a/options/url.go +++ b/options/url.go @@ -12,7 +12,11 @@ import ( const urlTokenPlain = "plain" -func addBaseURL(u string) string { +func preprocessURL(u string) string { + for re, repl := range config.URLReplacements { + u = re.ReplaceAllString(u, repl) + } + if len(config.BaseURL) == 0 || strings.HasPrefix(u, config.BaseURL) { return u } @@ -43,7 +47,7 @@ func decodeBase64URL(parts []string) (string, string, error) { return "", "", fmt.Errorf("Invalid url encoding: %s", encoded) } - return addBaseURL(string(imageURL)), format, nil + return preprocessURL(string(imageURL)), format, nil } func decodePlainURL(parts []string) (string, string, error) { @@ -69,7 +73,7 @@ func decodePlainURL(parts []string) (string, string, error) { return "", "", fmt.Errorf("Invalid url encoding: %s", encoded) } - return addBaseURL(unescaped), format, nil + return preprocessURL(unescaped), format, nil } func DecodeURL(parts []string) (string, string, error) {