From f100d3910b28a0732b8403c5022d8523e062a83b Mon Sep 17 00:00:00 2001 From: DarthSim Date: Thu, 6 Feb 2025 17:25:27 +0300 Subject: [PATCH] Fix handing 206 responses from source with full Content-Range --- CHANGELOG.md | 3 +++ imagedata/download.go | 47 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e0f66fb..547f3d25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Added - Add [IMGPROXY_BASE64_URL_INCLUDES_FILENAME](https://docs.imgproxy.net/latest/configuration/options#IMGPROXY_BASE64_URL_INCLUDES_FILENAME) config. +### Changed +- Treat 206 (Partial Content) responses from a source server as 200 (OK) when they contain a full content range. + ## [3.27.2] - 2025-01-27 ### Fixed - Fix preventing requests to `0.0.0.0` when imgproxy is configured to deny loopback addresses. diff --git a/imagedata/download.go b/imagedata/download.go index 43ffa780..c415a36b 100644 --- a/imagedata/download.go +++ b/imagedata/download.go @@ -7,6 +7,8 @@ import ( "io" "net/http" "net/http/cookiejar" + "regexp" + "strconv" "strings" "time" @@ -38,6 +40,8 @@ var ( "Last-Modified", } + contentRangeRe = regexp.MustCompile(`^bytes ((\d+)-(\d+)|\*)/(\d+|\*)$`) + // For tests redirectAllRequestsTo string ) @@ -225,8 +229,46 @@ func requestImage(ctx context.Context, imageURL string, opts DownloadOptions) (* return nil, func() {}, &ErrorNotModified{Message: "Not Modified", Headers: headersToStore(res)} } - if res.StatusCode != 200 { - body, _ := io.ReadAll(res.Body) + // If the source responds with 206, check if the response contains entire image. + // If not, return an error. + if res.StatusCode == http.StatusPartialContent { + contentRange := res.Header.Get("Content-Range") + rangeParts := contentRangeRe.FindStringSubmatch(contentRange) + if len(rangeParts) == 0 { + res.Body.Close() + reqCancel() + return nil, func() {}, ierrors.New(404, "Partial response with invalid Content-Range header", msgSourceImageIsUnreachable) + } + + if rangeParts[1] == "*" || rangeParts[2] != "0" { + res.Body.Close() + reqCancel() + return nil, func() {}, ierrors.New(404, "Partial response with incomplete content", msgSourceImageIsUnreachable) + } + + contentLengthStr := rangeParts[4] + if contentLengthStr == "*" { + contentLengthStr = res.Header.Get("Content-Length") + } + + contentLength, _ := strconv.Atoi(contentLengthStr) + rangeEnd, _ := strconv.Atoi(rangeParts[3]) + + if contentLength <= 0 || rangeEnd != contentLength-1 { + res.Body.Close() + reqCancel() + return nil, func() {}, ierrors.New(404, "Partial response with incomplete content", msgSourceImageIsUnreachable) + } + } else if res.StatusCode != http.StatusOK { + var msg string + + if strings.HasPrefix(res.Header.Get("Content-Type"), "text/") { + body, _ := io.ReadAll(io.LimitReader(res.Body, 1024)) + msg = fmt.Sprintf("Status: %d; %s", res.StatusCode, string(body)) + } else { + msg = fmt.Sprintf("Status: %d", res.StatusCode) + } + res.Body.Close() reqCancel() @@ -235,7 +277,6 @@ func requestImage(ctx context.Context, imageURL string, opts DownloadOptions) (* status = 500 } - msg := fmt.Sprintf("Status: %d; %s", res.StatusCode, string(body)) return nil, func() {}, ierrors.New(status, msg, msgSourceImageIsUnreachable) }