| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 | package fetcherimport (	"compress/gzip"	"context"	"io"	"net/http"	"net/http/cookiejar"	"net/url"	"regexp"	"strconv"	"strings"	"time"	"github.com/imgproxy/imgproxy/v3/httpheaders")var (	// contentRangeRe Content-Range header regex to check if the response is a partial content response	contentRangeRe = regexp.MustCompile(`^bytes ((\d+)-(\d+)|\*)/(\d+|\*)$`))// Request is a struct that holds the request and cancel function for an image fetcher requesttype Request struct {	fetcher *Fetcher           // Parent ImageFetcher instance	request *http.Request      // HTTP request to fetch the image	cancel  context.CancelFunc // Request context cancel function}// Send sends the generic request and returns the http.Response or an errorfunc (r *Request) Send() (*http.Response, error) {	client := r.fetcher.newHttpClient()	// Let's add a cookie jar to the client if the request URL is HTTP or HTTPS	// This is necessary to pass cookie challenge for some servers.	if r.request.URL.Scheme == "http" || r.request.URL.Scheme == "https" {		jar, err := cookiejar.New(nil)		if err != nil {			return nil, err		}		client.Jar = jar	}	for {		// Try request		res, err := client.Do(r.request)		if err == nil {			return res, nil // Return successful response		}		// Close the response body if request was unsuccessful		if res != nil && res.Body != nil {			res.Body.Close()		}		// Retry if the error is due to a lost connection		if strings.Contains(err.Error(), connectionLostError) {			select {			case <-r.request.Context().Done():				return nil, err			case <-time.After(bounceDelay):				continue			}		}		return nil, WrapError(err)	}}// Fetch fetches the image using the request and returns the response or an error.// Unlike Send, it checks the request status and converts it into typed errors.// Specifically, it checks for Not Modified, ensures that Partial Content response// contains the entire image, and wraps gzip-encoded responses.func (r *Request) Fetch() (*http.Response, error) {	res, err := r.Send()	if err != nil {		return nil, err	}	// If the source image was not modified, close the body and NotModifiedError	if res.StatusCode == http.StatusNotModified {		res.Body.Close()		return nil, newNotModifiedError(res.Header)	}	// If the source responds with 206, check if the response contains an entire image.	// If not, return an error.	if res.StatusCode == http.StatusPartialContent {		err = checkPartialContentResponse(res)		if err != nil {			res.Body.Close()			return nil, err		}	} else if res.StatusCode != http.StatusOK {		body := extractErraticBody(res)		res.Body.Close()		return nil, newImageResponseStatusError(res.StatusCode, body)	}	// If the response is gzip encoded, wrap it in a gzip reader	err = wrapGzipBody(res)	if err != nil {		res.Body.Close()		return nil, err	}	return res, nil}// Cancel cancels the request contextfunc (r *Request) Cancel() {	r.cancel()}// URL returns the actual URL of the requestfunc (r *Request) URL() *url.URL {	return r.request.URL}// checkPartialContentResponse if the response is a partial content response,// we check if it contains the entire image.func checkPartialContentResponse(res *http.Response) error {	contentRange := res.Header.Get(httpheaders.ContentRange)	rangeParts := contentRangeRe.FindStringSubmatch(contentRange)	if len(rangeParts) == 0 {		return newImagePartialResponseError("Partial response with invalid Content-Range header")	}	if rangeParts[1] == "*" || rangeParts[2] != "0" {		return newImagePartialResponseError("Partial response with incomplete content")	}	contentLengthStr := rangeParts[4]	if contentLengthStr == "*" {		contentLengthStr = res.Header.Get(httpheaders.ContentLength)	}	contentLength, _ := strconv.Atoi(contentLengthStr)	rangeEnd, _ := strconv.Atoi(rangeParts[3])	if contentLength <= 0 || rangeEnd != contentLength-1 {		return newImagePartialResponseError("Partial response with incomplete content")	}	return nil}// extractErraticBody extracts the error body from the response if it is a text-based content typefunc extractErraticBody(res *http.Response) string {	if strings.HasPrefix(res.Header.Get(httpheaders.ContentType), "text/") {		bbody, _ := io.ReadAll(io.LimitReader(res.Body, 1024))		return string(bbody)	}	return ""}// wrapGzipBody wraps the response body in a gzip reader if the Content-Encoding is gzip.// We set DisableCompression: true to avoid sending the Accept-Encoding: gzip header,// since we do not want to compress image data (which is usually already compressed).// However, some servers still send gzip-encoded responses regardless.func wrapGzipBody(res *http.Response) error {	if res.Header.Get(httpheaders.ContentEncoding) == "gzip" {		gzipBody, err := gzip.NewReader(res.Body)		if err != nil {			return nil		}		res.Body = &gzipReadCloser{			Reader: gzipBody,			r:      res.Body,		}		res.Header.Del(httpheaders.ContentEncoding)	}	return nil}// gzipReadCloser is a wrapper around gzip.Reader which also closes the original bodytype gzipReadCloser struct {	*gzip.Reader	r io.ReadCloser}// Close closes the gzip reader and the original bodyfunc (gr *gzipReadCloser) Close() error {	gr.Reader.Close()	return gr.r.Close()}
 |