etag.go 3.4 KB

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