writer.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. // headerwriter is responsible for writing processing/stream response headers
  2. package headerwriter
  3. import (
  4. "fmt"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "github.com/imgproxy/imgproxy/v3/httpheaders"
  10. )
  11. // Writer is a struct that creates header writer factories.
  12. type Writer struct {
  13. config *Config
  14. varyValue string
  15. }
  16. // Request is a private struct that builds HTTP response headers for a specific request.
  17. type Request struct {
  18. writer *Writer
  19. originHeaders http.Header // Original response headers
  20. result http.Header // Headers to be written to the response
  21. maxAge int // Current max age for Cache-Control header
  22. }
  23. // New creates a new header writer factory with the provided config.
  24. func New(config *Config) (*Writer, error) {
  25. if err := config.Validate(); err != nil {
  26. return nil, err
  27. }
  28. vary := make([]string, 0)
  29. if config.SetVaryAccept {
  30. vary = append(vary, "Accept")
  31. }
  32. if config.EnableClientHints {
  33. vary = append(vary, "Sec-CH-DPR", "DPR", "Sec-CH-Width", "Width")
  34. }
  35. varyValue := strings.Join(vary, ", ")
  36. return &Writer{
  37. config: config,
  38. varyValue: varyValue,
  39. }, nil
  40. }
  41. // NewRequest creates a new header writer instance for a specific request with the provided origin headers and URL.
  42. func (w *Writer) NewRequest() *Request {
  43. return &Request{
  44. writer: w,
  45. result: make(http.Header),
  46. maxAge: -1,
  47. originHeaders: make(http.Header),
  48. }
  49. }
  50. // SetOriginHeaders sets the origin headers for the request.
  51. func (r *Request) SetOriginHeaders(h http.Header) {
  52. r.originHeaders = h
  53. }
  54. // SetIsFallbackImage sets the Fallback-Image header to
  55. // indicate that the fallback image was used.
  56. func (r *Request) SetIsFallbackImage() {
  57. // We set maxAge to FallbackImageTTL if it's explicitly passed
  58. if r.writer.config.FallbackImageTTL < 0 {
  59. return
  60. }
  61. // However, we should not overwrite existing value if set (or greater than ours)
  62. if r.maxAge < 0 || r.maxAge > r.writer.config.FallbackImageTTL {
  63. r.maxAge = r.writer.config.FallbackImageTTL
  64. }
  65. }
  66. // SetExpires sets the TTL from time
  67. func (r *Request) SetExpires(expires *time.Time) {
  68. if expires == nil {
  69. return
  70. }
  71. // Convert current maxAge to time
  72. currentMaxAgeTime := time.Now().Add(time.Duration(r.maxAge) * time.Second)
  73. // If maxAge outlives expires or was not set, we'll use expires as maxAge.
  74. if r.maxAge < 0 || expires.Before(currentMaxAgeTime) {
  75. r.maxAge = min(r.writer.config.DefaultTTL, max(0, int(time.Until(*expires).Seconds())))
  76. }
  77. }
  78. // SetVary sets the Vary header
  79. func (r *Request) SetVary() {
  80. if len(r.writer.varyValue) > 0 {
  81. r.result.Set(httpheaders.Vary, r.writer.varyValue)
  82. }
  83. }
  84. // SetContentDisposition sets the Content-Disposition header, passthrough to ContentDispositionValue
  85. func (r *Request) SetContentDisposition(originURL, filename, ext, contentType string, returnAttachment bool) {
  86. value := httpheaders.ContentDispositionValue(
  87. originURL,
  88. filename,
  89. ext,
  90. contentType,
  91. returnAttachment,
  92. )
  93. if value != "" {
  94. r.result.Set(httpheaders.ContentDisposition, value)
  95. }
  96. }
  97. // Passthrough copies specified headers from the original response headers to the response headers.
  98. func (r *Request) Passthrough(only ...string) {
  99. httpheaders.Copy(r.originHeaders, r.result, only)
  100. }
  101. // CopyFrom copies specified headers from the headers object. Please note that
  102. // all the past operations may overwrite those values.
  103. func (r *Request) CopyFrom(headers http.Header, only []string) {
  104. httpheaders.Copy(headers, r.result, only)
  105. }
  106. // SetContentLength sets the Content-Length header
  107. func (r *Request) SetContentLength(contentLength int) {
  108. if contentLength < 0 {
  109. return
  110. }
  111. r.result.Set(httpheaders.ContentLength, strconv.Itoa(contentLength))
  112. }
  113. // SetContentType sets the Content-Type header
  114. func (r *Request) SetContentType(mime string) {
  115. r.result.Set(httpheaders.ContentType, mime)
  116. }
  117. // writeCanonical sets the Link header with the canonical URL.
  118. // It is mandatory for any response if enabled in the configuration.
  119. func (r *Request) SetCanonical(url string) {
  120. if !r.writer.config.SetCanonicalHeader {
  121. return
  122. }
  123. if strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://") {
  124. value := fmt.Sprintf(`<%s>; rel="canonical"`, url)
  125. r.result.Set(httpheaders.Link, value)
  126. }
  127. }
  128. // setCacheControl sets the Cache-Control header with the specified value.
  129. func (r *Request) setCacheControl(value int) bool {
  130. if value <= 0 {
  131. return false
  132. }
  133. r.result.Set(httpheaders.CacheControl, fmt.Sprintf("max-age=%d, public", value))
  134. return true
  135. }
  136. // setCacheControlNoCache sets the Cache-Control header to no-cache (default).
  137. func (r *Request) setCacheControlNoCache() {
  138. r.result.Set(httpheaders.CacheControl, "no-cache")
  139. }
  140. // setCacheControlPassthrough sets the Cache-Control header from the request
  141. // if passthrough is enabled in the configuration.
  142. func (r *Request) setCacheControlPassthrough() bool {
  143. if !r.writer.config.CacheControlPassthrough || r.maxAge > 0 {
  144. return false
  145. }
  146. if val := r.originHeaders.Get(httpheaders.CacheControl); val != "" {
  147. r.result.Set(httpheaders.CacheControl, val)
  148. return true
  149. }
  150. if val := r.originHeaders.Get(httpheaders.Expires); val != "" {
  151. if t, err := time.Parse(http.TimeFormat, val); err == nil {
  152. maxAge := max(0, int(time.Until(t).Seconds()))
  153. return r.setCacheControl(maxAge)
  154. }
  155. }
  156. return false
  157. }
  158. // setCSP sets the Content-Security-Policy header to prevent script execution.
  159. func (r *Request) setCSP() {
  160. r.result.Set(httpheaders.ContentSecurityPolicy, "script-src 'none'")
  161. }
  162. // Write writes the headers to the response writer. It does not overwrite
  163. // target headers, which were set outside the header writer.
  164. func (r *Request) Write(rw http.ResponseWriter) {
  165. // Then, let's try to set Cache-Control using priority order
  166. switch {
  167. case r.setCacheControl(r.maxAge): // First, try set explicit
  168. case r.setCacheControlPassthrough(): // Try to pick up from request headers
  169. case r.setCacheControl(r.writer.config.DefaultTTL): // Fallback to default value
  170. default:
  171. r.setCacheControlNoCache() // By default we use no-cache
  172. }
  173. r.setCSP()
  174. // Copy all headers to the response without overwriting existing ones
  175. httpheaders.CopyAll(r.result, rw.Header(), false)
  176. }