etag.go 3.3 KB

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