aboutsummaryrefslogtreecommitdiff
path: root/vendor/github.com/aws/aws-sdk-go/service/s3/body_hash.go
blob: 5c8ce5cc8a520ac2f35649bdf614209bd9f04561 (plain)
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
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"
	"github.com/aws/aws-sdk-go/internal/sdkio"
)

const (
	contentMD5Header    = "Content-Md5"
	contentSha256Header = "X-Amz-Content-Sha256"
	amzTeHeader         = "X-Amz-Te"
	amzTxEncodingHeader = "X-Amz-Transfer-Encoding"

	appendMD5TxEncoding = "append-md5"
)

// contentMD5 computes and sets the HTTP Content-MD5 header for requests that
// require it.
func contentMD5(r *request.Request) {
	h := md5.New()

	if !aws.IsReaderSeekable(r.Body) {
		if r.Config.Logger != nil {
			r.Config.Logger.Log(fmt.Sprintf(
				"Unable to compute Content-MD5 for unseekable body, S3.%s",
				r.Operation.Name))
		}
		return
	}

	if _, err := copySeekableBody(h, r.Body); err != nil {
		r.Error = awserr.New("ContentMD5", "failed to compute body MD5", err)
		return
	}

	// encode the md5 checksum in base64 and set the request header.
	v := base64.StdEncoding.EncodeToString(h.Sum(nil))
	r.HTTPRequest.Header.Set(contentMD5Header, v)
}

// 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 := 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
)

func copySeekableBody(dst io.Writer, src io.ReadSeeker) (int64, error) {
	curPos, err := src.Seek(0, sdkio.SeekCurrent)
	if err != nil {
		return 0, err
	}

	// hash the body.  seek back to the first position after reading to reset
	// the body for transmission.  copy errors may be assumed to be from the
	// body.
	n, err := io.Copy(dst, src)
	if err != nil {
		return n, err
	}

	_, err = src.Seek(curPos, sdkio.SeekStart)
	if err != nil {
		return n, err
	}

	return n, nil
}

// 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()
}