body_hash.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package s3
  2. import (
  3. "bytes"
  4. "crypto/md5"
  5. "crypto/sha256"
  6. "encoding/base64"
  7. "encoding/hex"
  8. "fmt"
  9. "hash"
  10. "io"
  11. "github.com/aws/aws-sdk-go/aws"
  12. "github.com/aws/aws-sdk-go/aws/awserr"
  13. "github.com/aws/aws-sdk-go/aws/request"
  14. "github.com/aws/aws-sdk-go/internal/sdkio"
  15. )
  16. const (
  17. contentMD5Header = "Content-Md5"
  18. contentSha256Header = "X-Amz-Content-Sha256"
  19. amzTeHeader = "X-Amz-Te"
  20. amzTxEncodingHeader = "X-Amz-Transfer-Encoding"
  21. appendMD5TxEncoding = "append-md5"
  22. )
  23. // contentMD5 computes and sets the HTTP Content-MD5 header for requests that
  24. // require it.
  25. func contentMD5(r *request.Request) {
  26. h := md5.New()
  27. if !aws.IsReaderSeekable(r.Body) {
  28. if r.Config.Logger != nil {
  29. r.Config.Logger.Log(fmt.Sprintf(
  30. "Unable to compute Content-MD5 for unseekable body, S3.%s",
  31. r.Operation.Name))
  32. }
  33. return
  34. }
  35. if _, err := copySeekableBody(h, r.Body); err != nil {
  36. r.Error = awserr.New("ContentMD5", "failed to compute body MD5", err)
  37. return
  38. }
  39. // encode the md5 checksum in base64 and set the request header.
  40. v := base64.StdEncoding.EncodeToString(h.Sum(nil))
  41. r.HTTPRequest.Header.Set(contentMD5Header, v)
  42. }
  43. // computeBodyHashes will add Content MD5 and Content Sha256 hashes to the
  44. // request. If the body is not seekable or S3DisableContentMD5Validation set
  45. // this handler will be ignored.
  46. func computeBodyHashes(r *request.Request) {
  47. if aws.BoolValue(r.Config.S3DisableContentMD5Validation) {
  48. return
  49. }
  50. if r.IsPresigned() {
  51. return
  52. }
  53. if r.Error != nil || !aws.IsReaderSeekable(r.Body) {
  54. return
  55. }
  56. var md5Hash, sha256Hash hash.Hash
  57. hashers := make([]io.Writer, 0, 2)
  58. // Determine upfront which hashes can be set without overriding user
  59. // provide header data.
  60. if v := r.HTTPRequest.Header.Get(contentMD5Header); len(v) == 0 {
  61. md5Hash = md5.New()
  62. hashers = append(hashers, md5Hash)
  63. }
  64. if v := r.HTTPRequest.Header.Get(contentSha256Header); len(v) == 0 {
  65. sha256Hash = sha256.New()
  66. hashers = append(hashers, sha256Hash)
  67. }
  68. // Create the destination writer based on the hashes that are not already
  69. // provided by the user.
  70. var dst io.Writer
  71. switch len(hashers) {
  72. case 0:
  73. return
  74. case 1:
  75. dst = hashers[0]
  76. default:
  77. dst = io.MultiWriter(hashers...)
  78. }
  79. if _, err := copySeekableBody(dst, r.Body); err != nil {
  80. r.Error = awserr.New("BodyHashError", "failed to compute body hashes", err)
  81. return
  82. }
  83. // For the hashes created, set the associated headers that the user did not
  84. // already provide.
  85. if md5Hash != nil {
  86. sum := make([]byte, md5.Size)
  87. encoded := make([]byte, md5Base64EncLen)
  88. base64.StdEncoding.Encode(encoded, md5Hash.Sum(sum[0:0]))
  89. r.HTTPRequest.Header[contentMD5Header] = []string{string(encoded)}
  90. }
  91. if sha256Hash != nil {
  92. encoded := make([]byte, sha256HexEncLen)
  93. sum := make([]byte, sha256.Size)
  94. hex.Encode(encoded, sha256Hash.Sum(sum[0:0]))
  95. r.HTTPRequest.Header[contentSha256Header] = []string{string(encoded)}
  96. }
  97. }
  98. const (
  99. md5Base64EncLen = (md5.Size + 2) / 3 * 4 // base64.StdEncoding.EncodedLen
  100. sha256HexEncLen = sha256.Size * 2 // hex.EncodedLen
  101. )
  102. func copySeekableBody(dst io.Writer, src io.ReadSeeker) (int64, error) {
  103. curPos, err := src.Seek(0, sdkio.SeekCurrent)
  104. if err != nil {
  105. return 0, err
  106. }
  107. // hash the body. seek back to the first position after reading to reset
  108. // the body for transmission. copy errors may be assumed to be from the
  109. // body.
  110. n, err := io.Copy(dst, src)
  111. if err != nil {
  112. return n, err
  113. }
  114. _, err = src.Seek(curPos, sdkio.SeekStart)
  115. if err != nil {
  116. return n, err
  117. }
  118. return n, nil
  119. }
  120. // Adds the x-amz-te: append_md5 header to the request. This requests the service
  121. // responds with a trailing MD5 checksum.
  122. //
  123. // Will not ask for append MD5 if disabled, the request is presigned or,
  124. // or the API operation does not support content MD5 validation.
  125. func askForTxEncodingAppendMD5(r *request.Request) {
  126. if aws.BoolValue(r.Config.S3DisableContentMD5Validation) {
  127. return
  128. }
  129. if r.IsPresigned() {
  130. return
  131. }
  132. r.HTTPRequest.Header.Set(amzTeHeader, appendMD5TxEncoding)
  133. }
  134. func useMD5ValidationReader(r *request.Request) {
  135. if r.Error != nil {
  136. return
  137. }
  138. if v := r.HTTPResponse.Header.Get(amzTxEncodingHeader); v != appendMD5TxEncoding {
  139. return
  140. }
  141. var bodyReader *io.ReadCloser
  142. var contentLen int64
  143. switch tv := r.Data.(type) {
  144. case *GetObjectOutput:
  145. bodyReader = &tv.Body
  146. contentLen = aws.Int64Value(tv.ContentLength)
  147. // Update ContentLength hiden the trailing MD5 checksum.
  148. tv.ContentLength = aws.Int64(contentLen - md5.Size)
  149. tv.ContentRange = aws.String(r.HTTPResponse.Header.Get("X-Amz-Content-Range"))
  150. default:
  151. r.Error = awserr.New("ChecksumValidationError",
  152. fmt.Sprintf("%s: %s header received on unsupported API, %s",
  153. amzTxEncodingHeader, appendMD5TxEncoding, r.Operation.Name,
  154. ), nil)
  155. return
  156. }
  157. if contentLen < md5.Size {
  158. r.Error = awserr.New("ChecksumValidationError",
  159. fmt.Sprintf("invalid Content-Length %d for %s %s",
  160. contentLen, appendMD5TxEncoding, amzTxEncodingHeader,
  161. ), nil)
  162. return
  163. }
  164. // Wrap and swap the response body reader with the validation reader.
  165. *bodyReader = newMD5ValidationReader(*bodyReader, contentLen-md5.Size)
  166. }
  167. type md5ValidationReader struct {
  168. rawReader io.ReadCloser
  169. payload io.Reader
  170. hash hash.Hash
  171. payloadLen int64
  172. read int64
  173. }
  174. func newMD5ValidationReader(reader io.ReadCloser, payloadLen int64) *md5ValidationReader {
  175. h := md5.New()
  176. return &md5ValidationReader{
  177. rawReader: reader,
  178. payload: io.TeeReader(&io.LimitedReader{R: reader, N: payloadLen}, h),
  179. hash: h,
  180. payloadLen: payloadLen,
  181. }
  182. }
  183. func (v *md5ValidationReader) Read(p []byte) (n int, err error) {
  184. n, err = v.payload.Read(p)
  185. if err != nil && err != io.EOF {
  186. return n, err
  187. }
  188. v.read += int64(n)
  189. if err == io.EOF {
  190. if v.read != v.payloadLen {
  191. return n, io.ErrUnexpectedEOF
  192. }
  193. expectSum := make([]byte, md5.Size)
  194. actualSum := make([]byte, md5.Size)
  195. if _, sumReadErr := io.ReadFull(v.rawReader, expectSum); sumReadErr != nil {
  196. return n, sumReadErr
  197. }
  198. actualSum = v.hash.Sum(actualSum[0:0])
  199. if !bytes.Equal(expectSum, actualSum) {
  200. return n, awserr.New("InvalidChecksum",
  201. fmt.Sprintf("expected MD5 checksum %s, got %s",
  202. hex.EncodeToString(expectSum),
  203. hex.EncodeToString(actualSum),
  204. ),
  205. nil)
  206. }
  207. }
  208. return n, err
  209. }
  210. func (v *md5ValidationReader) Close() error {
  211. return v.rawReader.Close()
  212. }