request.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. package fetcher
  2. import (
  3. "compress/gzip"
  4. "context"
  5. "io"
  6. "net/http"
  7. "net/http/cookiejar"
  8. "net/url"
  9. "regexp"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "github.com/imgproxy/imgproxy/v3/httpheaders"
  14. )
  15. var (
  16. // contentRangeRe Content-Range header regex to check if the response is a partial content response
  17. contentRangeRe = regexp.MustCompile(`^bytes ((\d+)-(\d+)|\*)/(\d+|\*)$`)
  18. )
  19. // Request is a struct that holds the request and cancel function for an image fetcher request
  20. type Request struct {
  21. fetcher *Fetcher // Parent ImageFetcher instance
  22. request *http.Request // HTTP request to fetch the image
  23. cancel context.CancelFunc // Request context cancel function
  24. }
  25. // Send sends the generic request and returns the http.Response or an error
  26. func (r *Request) Send() (*http.Response, error) {
  27. client := r.fetcher.newHttpClient()
  28. // Let's add a cookie jar to the client if the request URL is HTTP or HTTPS
  29. // This is necessary to pass cookie challenge for some servers.
  30. if r.request.URL.Scheme == "http" || r.request.URL.Scheme == "https" {
  31. jar, err := cookiejar.New(nil)
  32. if err != nil {
  33. return nil, err
  34. }
  35. client.Jar = jar
  36. }
  37. for {
  38. // Try request
  39. res, err := client.Do(r.request)
  40. if err == nil {
  41. return res, nil // Return successful response
  42. }
  43. // Close the response body if request was unsuccessful
  44. if res != nil && res.Body != nil {
  45. res.Body.Close()
  46. }
  47. // Retry if the error is due to a lost connection
  48. if strings.Contains(err.Error(), connectionLostError) {
  49. select {
  50. case <-r.request.Context().Done():
  51. return nil, err
  52. case <-time.After(bounceDelay):
  53. continue
  54. }
  55. }
  56. return nil, WrapError(err)
  57. }
  58. }
  59. // FetchImage fetches the image using the request and returns the response or an error.
  60. // It checks for the NotModified status and handles partial content responses.
  61. func (r *Request) FetchImage() (*http.Response, error) {
  62. res, err := r.Send()
  63. if err != nil {
  64. return nil, err
  65. }
  66. // If the source image was not modified, close the body and NotModifiedError
  67. if res.StatusCode == http.StatusNotModified {
  68. res.Body.Close()
  69. return nil, newNotModifiedError(res.Header)
  70. }
  71. // If the source responds with 206, check if the response contains an entire image.
  72. // If not, return an error.
  73. if res.StatusCode == http.StatusPartialContent {
  74. err = checkPartialContentResponse(res)
  75. if err != nil {
  76. res.Body.Close()
  77. return nil, err
  78. }
  79. } else if res.StatusCode != http.StatusOK {
  80. body := extractErraticBody(res)
  81. res.Body.Close()
  82. return nil, newImageResponseStatusError(res.StatusCode, body)
  83. }
  84. // If the response is gzip encoded, wrap it in a gzip reader
  85. err = wrapGzipBody(res)
  86. if err != nil {
  87. res.Body.Close()
  88. return nil, err
  89. }
  90. return res, nil
  91. }
  92. // Cancel cancels the request context
  93. func (r *Request) Cancel() {
  94. r.cancel()
  95. }
  96. // URL returns the actual URL of the request
  97. func (r *Request) URL() *url.URL {
  98. return r.request.URL
  99. }
  100. // checkPartialContentResponse if the response is a partial content response,
  101. // we check if it contains the entire image.
  102. func checkPartialContentResponse(res *http.Response) error {
  103. contentRange := res.Header.Get(httpheaders.ContentRange)
  104. rangeParts := contentRangeRe.FindStringSubmatch(contentRange)
  105. if len(rangeParts) == 0 {
  106. return newImagePartialResponseError("Partial response with invalid Content-Range header")
  107. }
  108. if rangeParts[1] == "*" || rangeParts[2] != "0" {
  109. return newImagePartialResponseError("Partial response with incomplete content")
  110. }
  111. contentLengthStr := rangeParts[4]
  112. if contentLengthStr == "*" {
  113. contentLengthStr = res.Header.Get(httpheaders.ContentLength)
  114. }
  115. contentLength, _ := strconv.Atoi(contentLengthStr)
  116. rangeEnd, _ := strconv.Atoi(rangeParts[3])
  117. if contentLength <= 0 || rangeEnd != contentLength-1 {
  118. return newImagePartialResponseError("Partial response with incomplete content")
  119. }
  120. return nil
  121. }
  122. // extractErraticBody extracts the error body from the response if it is a text-based content type
  123. func extractErraticBody(res *http.Response) string {
  124. if strings.HasPrefix(res.Header.Get(httpheaders.ContentType), "text/") {
  125. bbody, _ := io.ReadAll(io.LimitReader(res.Body, 1024))
  126. return string(bbody)
  127. }
  128. return ""
  129. }
  130. // wrapGzipBody wraps the response body in a gzip reader if the Content-Encoding is gzip.
  131. // We set DisableCompression: true to avoid sending the Accept-Encoding: gzip header,
  132. // since we do not want to compress image data (which is usually already compressed).
  133. // However, some servers still send gzip-encoded responses regardless.
  134. func wrapGzipBody(res *http.Response) error {
  135. if res.Header.Get(httpheaders.ContentEncoding) == "gzip" {
  136. gzipBody, err := gzip.NewReader(res.Body)
  137. if err != nil {
  138. return nil
  139. }
  140. res.Body = &gzipReadCloser{
  141. Reader: gzipBody,
  142. r: res.Body,
  143. }
  144. res.Header.Del(httpheaders.ContentEncoding)
  145. }
  146. return nil
  147. }
  148. // gzipReadCloser is a wrapper around gzip.Reader which also closes the original body
  149. type gzipReadCloser struct {
  150. *gzip.Reader
  151. r io.ReadCloser
  152. }
  153. // Close closes the gzip reader and the original body
  154. func (gr *gzipReadCloser) Close() error {
  155. gr.Reader.Close()
  156. return gr.r.Close()
  157. }