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