etag.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package etag
  2. import (
  3. "crypto/sha256"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "hash"
  8. "io"
  9. "net/http"
  10. "net/textproto"
  11. "strings"
  12. "sync"
  13. "github.com/imgproxy/imgproxy/v3/config"
  14. "github.com/imgproxy/imgproxy/v3/httpheaders"
  15. "github.com/imgproxy/imgproxy/v3/imagedata"
  16. "github.com/imgproxy/imgproxy/v3/options"
  17. )
  18. type eTagCalc struct {
  19. hash hash.Hash
  20. enc *json.Encoder
  21. }
  22. var eTagCalcPool = sync.Pool{
  23. New: func() interface{} {
  24. h := sha256.New()
  25. enc := json.NewEncoder(h)
  26. enc.SetEscapeHTML(false)
  27. enc.SetIndent("", "")
  28. return &eTagCalc{h, enc}
  29. },
  30. }
  31. type Handler struct {
  32. poHashActual, poHashExpected string
  33. imgEtagActual, imgEtagExpected string
  34. imgHashActual, imgHashExpected string
  35. }
  36. func (h *Handler) ParseExpectedETag(etag string) {
  37. // We suuport only a single ETag value
  38. if i := strings.IndexByte(etag, ','); i >= 0 {
  39. etag = textproto.TrimString(etag[:i])
  40. }
  41. etagLen := len(etag)
  42. // ETag is empty or invalid
  43. if etagLen < 2 {
  44. return
  45. }
  46. // We support strong ETags only
  47. if etag[0] != '"' || etag[etagLen-1] != '"' {
  48. return
  49. }
  50. // Remove quotes
  51. etag = etag[1 : etagLen-1]
  52. i := strings.Index(etag, "/")
  53. if i < 0 || i > etagLen-3 {
  54. // Doesn't look like imgproxy ETag
  55. return
  56. }
  57. poPart, imgPartMark, imgPart := etag[:i], etag[i+1], etag[i+2:]
  58. switch imgPartMark {
  59. case 'R':
  60. imgPartDec, err := base64.RawStdEncoding.DecodeString(imgPart)
  61. if err == nil {
  62. h.imgEtagExpected = string(imgPartDec)
  63. }
  64. case 'D':
  65. h.imgHashExpected = imgPart
  66. default:
  67. // Unknown image part mark
  68. return
  69. }
  70. h.poHashExpected = poPart
  71. }
  72. func (h *Handler) ProcessingOptionsMatch() bool {
  73. return h.poHashActual == h.poHashExpected
  74. }
  75. func (h *Handler) SetActualProcessingOptions(po *options.ProcessingOptions) bool {
  76. c := eTagCalcPool.Get().(*eTagCalc)
  77. defer eTagCalcPool.Put(c)
  78. c.hash.Reset()
  79. c.hash.Write([]byte(config.ETagBuster))
  80. c.enc.Encode(po)
  81. h.poHashActual = base64.RawURLEncoding.EncodeToString(c.hash.Sum(nil))
  82. return h.ProcessingOptionsMatch()
  83. }
  84. func (h *Handler) ImageEtagExpected() string {
  85. return h.imgEtagExpected
  86. }
  87. func (h *Handler) SetActualImageData(imgdata imagedata.ImageData, headers http.Header) (bool, error) {
  88. var haveActualImgETag bool
  89. h.imgEtagActual = headers.Get(httpheaders.Etag)
  90. haveActualImgETag = len(h.imgEtagActual) > 0
  91. // Just in case server didn't check ETag properly and returned the same one
  92. // as we expected
  93. if haveActualImgETag && h.imgEtagExpected == h.imgEtagActual {
  94. return true, nil
  95. }
  96. haveExpectedImgHash := len(h.imgHashExpected) != 0
  97. if !haveActualImgETag || haveExpectedImgHash {
  98. c := eTagCalcPool.Get().(*eTagCalc)
  99. defer eTagCalcPool.Put(c)
  100. c.hash.Reset()
  101. _, err := io.Copy(c.hash, imgdata.Reader())
  102. if err != nil {
  103. return false, err
  104. }
  105. h.imgHashActual = base64.RawURLEncoding.EncodeToString(c.hash.Sum(nil))
  106. return haveExpectedImgHash && h.imgHashActual == h.imgHashExpected, nil
  107. }
  108. return false, nil
  109. }
  110. func (h *Handler) GenerateActualETag() string {
  111. return h.generate(h.poHashActual, h.imgEtagActual, h.imgHashActual)
  112. }
  113. func (h *Handler) GenerateExpectedETag() string {
  114. return h.generate(h.poHashExpected, h.imgEtagExpected, h.imgHashExpected)
  115. }
  116. func (h *Handler) generate(poHash, imgEtag, imgHash string) string {
  117. imgPartMark := 'D'
  118. imgPart := imgHash
  119. if len(imgEtag) != 0 {
  120. imgPartMark = 'R'
  121. imgPart = base64.RawURLEncoding.EncodeToString([]byte(imgEtag))
  122. }
  123. return fmt.Sprintf(`"%s/%c%s"`, poHash, imgPartMark, imgPart)
  124. }