diff --git a/handler/remote.go b/handler/remote.go index e7c24bbd..588f5ed0 100644 --- a/handler/remote.go +++ b/handler/remote.go @@ -113,14 +113,14 @@ func fetchRemoteImg(url string, subdir string) (metaContent config.MetaFile) { // remote file has changed log.Info("Remote file changed, updating metadata and fetching image source...") helper.DeleteMetadata(url, subdir) - helper.WriteMetadata(url, etag, subdir) + metadata = helper.WriteMetadata(url, etag, subdir) } else { // local file not exists log.Info("Remote file not found in remote-raw, re-fetching...") } _ = downloadFile(localRawImagePath, url) // Update metadata with newly downloaded file - helper.WriteMetadata(url, etag, subdir) + metadata = helper.WriteMetadata(url, etag, subdir) } return metadata } diff --git a/handler/router.go b/handler/router.go index 50a81c09..9cd979a7 100644 --- a/handler/router.go +++ b/handler/router.go @@ -136,10 +136,12 @@ func Convert(c *fiber.Ctx) error { // this is proxyMode, we'll have to use this url to download and save it to local path, which also gives us rawImageAbs // https://test.webp.sh/mypic/123.jpg?someother=200&somebugs=200 + helper.SetMetedataProxyMode(true) metadata = fetchRemoteImg(realRemoteAddr, targetHostName) rawImageAbs = path.Join(config.Config.RemoteRawPath, targetHostName, metadata.Id) + path.Ext(realRemoteAddr) } else { // not proxyMode, we'll use local path + helper.SetMetedataProxyMode(false) metadata = helper.ReadMetadata(reqURIwithQuery, "", targetHostName) if !mapMode { // by default images are hosted in ImgPath @@ -150,7 +152,7 @@ func Convert(c *fiber.Ctx) error { // detect if source file has changed if metadata.Checksum != helper.HashFile(rawImageAbs) { log.Info("Source file has changed, re-encoding...") - helper.WriteMetadata(reqURIwithQuery, "", targetHostName) + metadata = helper.WriteMetadata(reqURIwithQuery, "", targetHostName) cleanProxyCache(path.Join(config.Config.ExhaustPath, targetHostName, metadata.Id)) } } @@ -168,6 +170,15 @@ func Convert(c *fiber.Ctx) error { }) } + if config.Config.EnableExtraParams && helper.HasResizeParams(extraParams) { + if err := helper.ValidateNoUpscale(metadata.ImageMeta, extraParams); err != nil { + log.Warnf("Blocked resize request for %s: %v", reqURIwithQuery, err) + c.Status(http.StatusBadRequest) + _ = c.SendString(err.Error()) + return nil + } + } + supportedFormats := helper.GuessSupportedFormat(reqHeader) // resize itself and return if only raw(jpg,jpeg,png,gif) is supported if supportedFormats["jpg"] == true && diff --git a/helper/helper.go b/helper/helper.go index d26cbd9c..910d3765 100644 --- a/helper/helper.go +++ b/helper/helper.go @@ -1,6 +1,7 @@ package helper import ( + "errors" "fmt" "os" "path" @@ -25,6 +26,7 @@ import ( var ( boolFalse vips.BoolParameter intMinusOne vips.IntParameter + ErrUpscaleNotAllowed = errors.New("requested resize exceeds source dimensions") ) var _ = filetype.AddMatcher(filetype.NewType("svg", "image/svg+xml"), svgMatcher) @@ -229,3 +231,23 @@ func HashFile(filepath string) string { buf, _ := os.ReadFile(filepath) return fmt.Sprintf("%x", xxhash.Sum64(buf)) } + +func HasResizeParams(extra config.ExtraParams) bool { + return extra.Width > 0 || extra.Height > 0 || extra.MaxWidth > 0 || extra.MaxHeight > 0 +} + +func ValidateNoUpscale(meta config.ImageMeta, extra config.ExtraParams) error { + if !HasResizeParams(extra) || meta.Width == 0 || meta.Height == 0 { + return nil + } + + if extra.Width > 0 && extra.Width > meta.Width { + return fmt.Errorf("%w: requested width %d exceeds source width %d", ErrUpscaleNotAllowed, extra.Width, meta.Width) + } + + if extra.Height > 0 && extra.Height > meta.Height { + return fmt.Errorf("%w: requested height %d exceeds source height %d", ErrUpscaleNotAllowed, extra.Height, meta.Height) + } + + return nil +} \ No newline at end of file diff --git a/helper/helper_test.go b/helper/helper_test.go index ece44508..79197bb3 100644 --- a/helper/helper_test.go +++ b/helper/helper_test.go @@ -164,3 +164,79 @@ func TestGuessSupportedFormat(t *testing.T) { }) } } + +func TestHasResizeParams(t *testing.T) { + if HasResizeParams(config.ExtraParams{}) { + t.Fatalf("expected empty params to report no resize request") + } + + params := config.ExtraParams{Width: 100} + if !HasResizeParams(params) { + t.Fatalf("expected Width to trigger resize detection") + } + + params = config.ExtraParams{MaxHeight: 80} + if !HasResizeParams(params) { + t.Fatalf("expected MaxHeight to trigger resize detection") + } +} + +func TestValidateNoUpscale(t *testing.T) { + meta := config.ImageMeta{Width: 200, Height: 100} + + tests := []struct { + params config.ExtraParams + wantError bool + }{ + // Tests for no resize params + { + params: config.ExtraParams{}, + wantError: false, + }, + // Tests for shrink Width only + { + params: config.ExtraParams{Width: 150}, + wantError: false, + }, + // Tests for shrink Height only + { + params: config.ExtraParams{Height: 80}, + wantError: false, + }, + // Tests for shrink both Width and Height + { + params: config.ExtraParams{Width: 150, Height: 80}, + wantError: false, + }, + // Tests for upscale Width only + { + params: config.ExtraParams{Width: 250}, + wantError: true, + }, + // Tests for upscale Height only + { + params: config.ExtraParams{Height: 150}, + wantError: true, + }, + // Tests for upscale both Width and Height + { + params: config.ExtraParams{Width: 300, Height: 200}, + wantError: true, + }, + // Tests for MaxWidth and MaxHeight + { + params: config.ExtraParams{MaxWidth: 300, MaxHeight: 400}, + wantError: false, + }, + } + + for _, tc := range tests { + err := ValidateNoUpscale(meta, tc.params) + if tc.wantError && err == nil { + t.Fatalf("TestValidateNoUpscale failed with error %s", err) + } + if !tc.wantError && err != nil { + t.Fatalf("TestValidateNoUpscale failed with error %s", err) + } + } +} \ No newline at end of file diff --git a/helper/metadata.go b/helper/metadata.go index 949e3ae0..b9c954c0 100644 --- a/helper/metadata.go +++ b/helper/metadata.go @@ -12,10 +12,14 @@ import ( log "github.com/sirupsen/logrus" ) +var ( + ProxyMode = false +) // Get ID and filepath // For ProxyMode, pass in p the remote-raw path + func getId(p string, subdir string) (id string, filePath string, santizedPath string) { - if config.ProxyMode { + if ProxyMode { fileID := HashString(p) return fileID, path.Join(config.Config.RemoteRawPath, subdir, fileID) + path.Ext(p), "" } @@ -71,7 +75,9 @@ func WriteMetadata(p, etag string, subdir string) config.MetaFile { } // Only get image metadata if the file has image extension - if CheckImageExtension(filepath) { + // extract path from URL + parsedURL, _ := url.Parse(filepath) + if CheckImageExtension(parsedURL.Path) { imageMeta := getImageMeta(filepath) data.ImageMeta = imageMeta } @@ -179,3 +185,7 @@ func DeleteMetadata(p string, subdir string) { log.Warnln("failed to delete metadata", err) } } + +func SetMetedataProxyMode(proxy bool) { + ProxyMode = proxy +} \ No newline at end of file diff --git a/helper/metadata_test.go b/helper/metadata_test.go index ad54d9e3..beb1e450 100644 --- a/helper/metadata_test.go +++ b/helper/metadata_test.go @@ -12,7 +12,7 @@ func TestGetId(t *testing.T) { t.Run("proxy mode", func(t *testing.T) { // Test case 1: Proxy mode - config.ProxyMode = true + SetMetedataProxyMode(true) id, jointPath, santizedPath := getId(p, "") // Verify the return values @@ -26,7 +26,7 @@ func TestGetId(t *testing.T) { }) t.Run("non-proxy mode", func(t *testing.T) { // Test case 2: Non-proxy mode - config.ProxyMode = false + SetMetedataProxyMode(false) p = "/image.jpg?width=400&height=500" id, jointPath, santizedPath := getId(p, "")