Преглед на файлове

Merge branch 'master' into version/3

DarthSim преди 3 години
родител
ревизия
d016e26e2e
променени са 10 файла, в които са добавени 155 реда и са изтрити 32 реда
  1. 17 0
      CHANGELOG.md
  2. 4 3
      config/config.go
  3. 33 0
      config/configurators/configurators.go
  4. 2 2
      docs/configuration.md
  5. 16 0
      imagedata/download.go
  6. 1 1
      landing.go
  7. 10 6
      options/url.go
  8. 62 15
      processing_handler_test.go
  9. 2 4
      security/source.go
  10. 8 1
      vips/vips.c

+ 17 - 0
CHANGELOG.md

@@ -17,6 +17,23 @@
 - Removed `crop` resizing type, use [crop](./docs/generating_the_url.md#crop) processing option instead.
 - Dropped old libvips (<8.8) support.
 
+## [2.17.0] - 2021-09-07
+### Added
+- Wildcard support in `IMGPROXY_ALLOWED_SOURCES`.
+
+### Change
+- If the source URL contains the `IMGPROXY_BASE_URL` prefix, it won't be added.
+
+### Fix
+- (pro) Fix path prefix support in the `/info` handler.
+
+### Deprecated
+- The [basic URL format](https://docs.imgproxy.net/generating_the_url_basic) is deprecated and can be removed in future versions. Use [advanced URL format](https://docs.imgproxy.net/generating_the_url_advanced) instead.
+
+## [2.16.7] - 2021-07-20
+### Change
+- Reset DPI while stripping meta.
+
 ## [2.16.6] - 2021-07-08
 ### Fix
 - Fix performance regression in ICC profile handling.

+ 4 - 3
config/config.go

@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"math"
 	"os"
+	"regexp"
 	"runtime"
 
 	log "github.com/sirupsen/logrus"
@@ -72,7 +73,7 @@ var (
 	IgnoreSslVerification bool
 	DevelopmentErrorsMode bool
 
-	AllowedSources      []string
+	AllowedSources      []*regexp.Regexp
 	LocalFileSystemRoot string
 	S3Enabled           bool
 	S3Region            string
@@ -200,7 +201,7 @@ func Reset() {
 	IgnoreSslVerification = false
 	DevelopmentErrorsMode = false
 
-	AllowedSources = make([]string, 0)
+	AllowedSources = make([]*regexp.Regexp, 0)
 	LocalFileSystemRoot = ""
 	S3Enabled = false
 	S3Region = ""
@@ -290,7 +291,7 @@ func Configure() error {
 
 	configurators.Int(&MaxAnimationFrames, "IMGPROXY_MAX_ANIMATION_FRAMES")
 
-	configurators.StringSlice(&AllowedSources, "IMGPROXY_ALLOWED_SOURCES")
+	configurators.Patterns(&AllowedSources, "IMGPROXY_ALLOWED_SOURCES")
 
 	configurators.Bool(&JpegProgressive, "IMGPROXY_JPEG_PROGRESSIVE")
 	configurators.Bool(&PngInterlaced, "IMGPROXY_PNG_INTERLACED")

+ 33 - 0
config/configurators/configurators.go

@@ -5,6 +5,7 @@ import (
 	"encoding/hex"
 	"fmt"
 	"os"
+	"regexp"
 	"strconv"
 	"strings"
 
@@ -184,3 +185,35 @@ func HexFile(b *[][]byte, filepath string) error {
 
 	return nil
 }
+
+func Patterns(s *[]*regexp.Regexp, name string) {
+	if env := os.Getenv(name); len(env) > 0 {
+		parts := strings.Split(env, ",")
+		result := make([]*regexp.Regexp, len(parts))
+
+		for i, p := range parts {
+			result[i] = RegexpFromPattern(strings.TrimSpace(p))
+		}
+
+		*s = result
+	} else {
+		*s = []*regexp.Regexp{}
+	}
+}
+
+func RegexpFromPattern(pattern string) *regexp.Regexp {
+	var result strings.Builder
+	// Perform prefix matching
+	result.WriteString("^")
+	for i, part := range strings.Split(pattern, "*") {
+		// Add a regexp match all without slashes for each wildcard character
+		if i > 0 {
+			result.WriteString("[^/]*")
+		}
+
+		// Quote other parts of the pattern
+		result.WriteString(regexp.QuoteMeta(part))
+	}
+	// It is safe to use regexp.MustCompile since the expression is always valid
+	return regexp.MustCompile(result.String())
+}

+ 2 - 2
docs/configuration.md

@@ -73,7 +73,7 @@ imgproxy does not send CORS headers by default. Specify allowed origin to enable
 
 You can limit allowed source URLs:
 
-* `IMGPROXY_ALLOWED_SOURCES`: whitelist of source image URLs prefixes divided by comma. When blank, imgproxy allows all source image URLs. Example: `s3://,https://example.com/,local://`. Default: blank.
+* `IMGPROXY_ALLOWED_SOURCES`: whitelist of source image URLs prefixes divided by comma. Wildcards can be included with `*` to match all characters except `/`. When blank, imgproxy allows all source image URLs. Example: `s3://,https://*.example.com/,local://`. Default: blank.
 
 **⚠️Warning:** Be careful when using this config to limit source URL hosts, and always add a trailing slash after the host. Bad: `http://example.com`, good: `http://example.com/`. If you don't add a trailing slash, `http://example.com@baddomain.com` will be an allowed URL but the request will be made to `baddomain.com`.
 
@@ -335,7 +335,7 @@ imgproxy can send logs to syslog, but this feature is disabled by default. To en
 
 ## Miscellaneous
 
-* `IMGPROXY_BASE_URL`: base URL prefix that will be added to every 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`. Default: blank.
+* `IMGPROXY_BASE_URL`: base URL prefix that will be added to every 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 JPEG and WebP. Allows to process the whole 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`.

+ 16 - 0
imagedata/download.go

@@ -25,6 +25,9 @@ var (
 		"Cache-Control",
 		"Expires",
 	}
+
+	// For tests
+	redirectAllRequestsTo string
 )
 
 const msgSourceImageIsUnreachable = "Source image is unreachable"
@@ -103,6 +106,11 @@ func requestImage(imageURL string) (*http.Response, error) {
 }
 
 func download(imageURL string) (*ImageData, error) {
+	// We use this for testing
+	if len(redirectAllRequestsTo) > 0 {
+		imageURL = redirectAllRequestsTo
+	}
+
 	res, err := requestImage(imageURL)
 	if res != nil {
 		defer res.Body.Close()
@@ -140,3 +148,11 @@ func download(imageURL string) (*ImageData, error) {
 
 	return imgdata, nil
 }
+
+func RedirectAllRequestsTo(u string) {
+	redirectAllRequestsTo = u
+}
+
+func StopRedirectingRequests() {
+	redirectAllRequestsTo = ""
+}

+ 1 - 1
landing.go

@@ -14,7 +14,7 @@ var landingTmpl = []byte(`
 `)
 
 func handleLanding(reqID string, rw http.ResponseWriter, r *http.Request) {
-	rw.Header().Set("Content-Tyle", "text/html")
+	rw.Header().Set("Content-Type", "text/html")
 	rw.WriteHeader(200)
 	rw.Write(landingTmpl)
 }

+ 10 - 6
options/url.go

@@ -12,6 +12,14 @@ import (
 
 const urlTokenPlain = "plain"
 
+func addBaseURL(u string) string {
+	if len(config.BaseURL) == 0 || strings.HasPrefix(u, config.BaseURL) {
+		return u
+	}
+
+	return fmt.Sprintf("%s%s", config.BaseURL, u)
+}
+
 func decodeBase64URL(parts []string) (string, string, error) {
 	var format string
 
@@ -35,9 +43,7 @@ func decodeBase64URL(parts []string) (string, string, error) {
 		return "", "", fmt.Errorf("Invalid url encoding: %s", encoded)
 	}
 
-	fullURL := fmt.Sprintf("%s%s", config.BaseURL, string(imageURL))
-
-	return fullURL, format, nil
+	return addBaseURL(string(imageURL)), format, nil
 }
 
 func decodePlainURL(parts []string) (string, string, error) {
@@ -63,9 +69,7 @@ func decodePlainURL(parts []string) (string, string, error) {
 		return "", "", fmt.Errorf("Invalid url encoding: %s", encoded)
 	}
 
-	fullURL := fmt.Sprintf("%s%s", config.BaseURL, unescaped)
-
-	return fullURL, format, nil
+	return addBaseURL(unescaped), format, nil
 }
 
 func DecodeURL(parts []string) (string, string, error) {

+ 62 - 15
processing_handler_test.go

@@ -7,9 +7,12 @@ import (
 	"net/http/httptest"
 	"os"
 	"path/filepath"
+	"regexp"
 	"testing"
 
 	"github.com/imgproxy/imgproxy/v2/config"
+	"github.com/imgproxy/imgproxy/v2/config/configurators"
+	"github.com/imgproxy/imgproxy/v2/imagedata"
 	"github.com/imgproxy/imgproxy/v2/imagemeta"
 	"github.com/imgproxy/imgproxy/v2/imagetype"
 	"github.com/imgproxy/imgproxy/v2/router"
@@ -115,22 +118,66 @@ func (s *ProcessingHandlerTestSuite) TestSignatureValidationSuccess() {
 	assert.Equal(s.T(), 200, res.StatusCode)
 }
 
-func (s *ProcessingHandlerTestSuite) TestSourceValidationFailure() {
-	config.AllowedSources = []string{"https://"}
-
-	rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
-	res := rw.Result()
-
-	assert.Equal(s.T(), 404, res.StatusCode)
-}
-
-func (s *ProcessingHandlerTestSuite) TestSourceValidationSuccess() {
-	config.AllowedSources = []string{"local:///"}
-
-	rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
-	res := rw.Result()
+func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
+	imagedata.RedirectAllRequestsTo("local:///test1.png")
+	defer imagedata.StopRedirectingRequests()
+
+	tt := []struct {
+		name           string
+		allowedSources []string
+		requestPath    string
+		expectedError  bool
+	}{
+		{
+			name:           "match http URL without wildcard",
+			allowedSources: []string{"local://", "http://images.dev/"},
+			requestPath:    "/unsafe/plain/http://images.dev/lorem/ipsum.jpg",
+			expectedError:  false,
+		},
+		{
+			name:           "match http URL with wildcard in hostname single level",
+			allowedSources: []string{"local://", "http://*.mycdn.dev/"},
+			requestPath:    "/unsafe/plain/http://a-1.mycdn.dev/lorem/ipsum.jpg",
+			expectedError:  false,
+		},
+		{
+			name:           "match http URL with wildcard in hostname multiple levels",
+			allowedSources: []string{"local://", "http://*.mycdn.dev/"},
+			requestPath:    "/unsafe/plain/http://a-1.b-2.mycdn.dev/lorem/ipsum.jpg",
+			expectedError:  false,
+		},
+		{
+			name:           "no match s3 URL with allowed local and http URLs",
+			allowedSources: []string{"local://", "http://images.dev/"},
+			requestPath:    "/unsafe/plain/s3://images/lorem/ipsum.jpg",
+			expectedError:  true,
+		},
+		{
+			name:           "no match http URL with wildcard in hostname including slash",
+			allowedSources: []string{"local://", "http://*.mycdn.dev/"},
+			requestPath:    "/unsafe/plain/http://other.dev/.mycdn.dev/lorem/ipsum.jpg",
+			expectedError:  true,
+		},
+	}
 
-	assert.Equal(s.T(), 200, res.StatusCode)
+	for _, tc := range tt {
+		s.T().Run(tc.name, func(t *testing.T) {
+			exps := make([]*regexp.Regexp, len(tc.allowedSources))
+			for i, pattern := range tc.allowedSources {
+				exps[i] = configurators.RegexpFromPattern(pattern)
+			}
+			config.AllowedSources = exps
+
+			rw := s.send(tc.requestPath)
+			res := rw.Result()
+
+			if tc.expectedError {
+				assert.Equal(s.T(), 404, res.StatusCode)
+			} else {
+				assert.Equal(s.T(), 200, res.StatusCode)
+			}
+		})
+	}
 }
 
 func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {

+ 2 - 4
security/source.go

@@ -1,8 +1,6 @@
 package security
 
 import (
-	"strings"
-
 	"github.com/imgproxy/imgproxy/v2/config"
 )
 
@@ -10,8 +8,8 @@ func VerifySourceURL(imageURL string) bool {
 	if len(config.AllowedSources) == 0 {
 		return true
 	}
-	for _, val := range config.AllowedSources {
-		if strings.HasPrefix(imageURL, string(val)) {
+	for _, allowedSource := range config.AllowedSources {
+		if allowedSource.MatchString(imageURL) {
 			return true
 		}
 	}

+ 8 - 1
vips/vips.c

@@ -523,7 +523,14 @@ vips_arrayjoin_go(VipsImage **in, VipsImage **out, int n) {
 
 int
 vips_strip(VipsImage *in, VipsImage **out) {
-  if (vips_copy(in, out, NULL)) return 1;
+  static double default_resolution = 72.0 / 25.4;
+
+  if (vips_copy(
+    in, out,
+    "xres", default_resolution,
+    "yres", default_resolution,
+    NULL
+  )) return 1;
 
   gchar **fields = vips_image_get_fields(in);