1 /*
2  * Copyright 2013-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License").
5  * You may not use this file except in compliance with the License.
6  * A copy of the License is located at
7  *
8  *  http://aws.amazon.com/apache2.0
9  *
10  * or in the "license" file accompanying this file. This file is distributed
11  * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12  * express or implied. See the License for the specific language governing
13  * permissions and limitations under the License.
14  */

15 package com.amazonaws.services.s3.internal;
16
17 import com.amazonaws.SdkClientException;
18 import com.amazonaws.ReadLimitInfo;
19 import com.amazonaws.Request;
20 import com.amazonaws.ResetException;
21 import com.amazonaws.SignableRequest;
22 import com.amazonaws.auth.AWS4Signer;
23 import com.amazonaws.auth.AwsChunkedEncodingInputStream;
24 import com.amazonaws.auth.internal.AWS4SignerRequestParams;
25 import com.amazonaws.services.s3.Headers;
26 import com.amazonaws.services.s3.model.PutObjectRequest;
27 import com.amazonaws.services.s3.model.UploadPartRequest;
28 import com.amazonaws.services.s3.request.S3HandlerContextKeys;
29 import com.amazonaws.util.BinaryUtils;
30
31 import java.io.IOException;
32 import java.io.InputStream;
33
34 import static com.amazonaws.auth.internal.SignerConstants.X_AMZ_CONTENT_SHA256;
35
36 /**
37  * AWS4 signer implementation for AWS S3
38  */

39 public class AWSS3V4Signer extends AWS4Signer {
40     private static final String CONTENT_SHA_256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
41
42     /** Sent to S3 in lieu of a payload hash when unsigned payloads are enabled */
43     private static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
44
45     /**
46      * Don't double-url-encode path elements; S3 expects path elements to be encoded only once in
47      * the canonical URI.
48      */

49     public AWSS3V4Signer() {
50         super(false);
51     }
52
53     /**
54      * If necessary, creates a chunk-encoding wrapper on the request payload.
55      */

56     @Override
57     protected void processRequestPayload(SignableRequest<?> request, byte[] signature,
58             byte[] signingKey, AWS4SignerRequestParams signerRequestParams) {
59         if (useChunkEncoding(request)) {
60             AwsChunkedEncodingInputStream chunkEncodededStream = new AwsChunkedEncodingInputStream(
61                     request.getContent(), signingKey,
62                     signerRequestParams.getFormattedSigningDateTime(),
63                     signerRequestParams.getScope(),
64                     BinaryUtils.toHex(signature), this);
65             request.setContent(chunkEncodededStream);
66         }
67     }
68
69     @Override
70     protected String calculateContentHashPresign(SignableRequest<?> request){
71         return "UNSIGNED-PAYLOAD";
72     }
73
74     /**
75      * Returns the pre-defined header value and set other necessary headers if
76      * the request needs to be chunk-encoded. Otherwise calls the superclass
77      * method which calculates the hash of the whole content for signing.
78      */

79     @Override
80     protected String calculateContentHash(SignableRequest<?> request) {
81         // To be consistent with other service clients using sig-v4,
82         // we just set the header as "required", and AWS4Signer.sign() will be
83         // notified to pick up the header value returned by this method.
84         request.addHeader(X_AMZ_CONTENT_SHA256, "required");
85
86         if (isPayloadSigningEnabled(request)) {
87             if (useChunkEncoding(request)) {
88                 final String contentLength = request.getHeaders().get(Headers.CONTENT_LENGTH);
89                 final long originalContentLength;
90                 if (contentLength != null) {
91                     originalContentLength = Long.parseLong(contentLength);
92                 } else {
93                     /**
94                      * "Content-Length" header could be missing if the caller is
95                      * uploading a stream without setting Content-Length in
96                      * ObjectMetadata. Before using sigv4, we rely on HttpClient to
97                      * add this header by using BufferedHttpEntity when creating the
98                      * HttpRequest object. But now, we need this information
99                      * immediately for the signing process, so we have to cache the
100                      * stream here.
101                      */

102                     try {
103                         originalContentLength = getContentLength(request);
104                     } catch (IOException e) {
105                         throw new SdkClientException(
106                                 "Cannot get the content-length of the request content.", e);
107                     }
108                 }
109                 request.addHeader("x-amz-decoded-content-length",
110                                   Long.toString(originalContentLength));
111                 // Make sure "Content-Length" header is not empty so that HttpClient
112                 // won't cache the stream again to recover Content-Length
113                 request.addHeader(Headers.CONTENT_LENGTH, Long.toString(
114                         AwsChunkedEncodingInputStream
115                                 .calculateStreamContentLength(originalContentLength)));
116                 return CONTENT_SHA_256;
117             } else {
118                 return super.calculateContentHash(request);
119             }
120         }
121
122         return UNSIGNED_PAYLOAD;
123     }
124
125     /**
126      * Determine whether to use aws-chunked for signing
127      */

128     private boolean useChunkEncoding(SignableRequest<?> request) {
129         // If chunked encoding is explicitly disabled through client options return right here.
130         // Chunked encoding only makes sense to do when the payload is signed
131         if (!isPayloadSigningEnabled(request) || isChunkedEncodingDisabled(request)) {
132             return false;
133         }
134         if (request.getOriginalRequestObject() instanceof PutObjectRequest
135                 || request.getOriginalRequestObject() instanceof UploadPartRequest) {
136             return true;
137         }
138         return false;
139     }
140
141     /**
142      * @return True if chunked encoding has been explicitly disabled per the request. False
143      *         otherwise.
144      */

145     private boolean isChunkedEncodingDisabled(SignableRequest<?> signableRequest) {
146         if (signableRequest instanceof Request) {
147             Request<?> request = (Request<?>) signableRequest;
148             Boolean isChunkedEncodingDisabled = request
149                     .getHandlerContext(S3HandlerContextKeys.IS_CHUNKED_ENCODING_DISABLED);
150             return isChunkedEncodingDisabled != null && isChunkedEncodingDisabled;
151         }
152         return false;
153     }
154
155     /**
156      * @return True if payload signing is explicitly enabled.
157      */

158     private boolean isPayloadSigningEnabled(SignableRequest<?> signableRequest) {
159         /**
160          * If we aren't using https we should always sign the payload.
161          */

162         if (!signableRequest.getEndpoint().getScheme().equals("https")) {
163             return true;
164         }
165
166         if (signableRequest instanceof Request) {
167             Request<?> request = (Request<?>) signableRequest;
168             Boolean isPayloadSigningEnabled = request
169                     .getHandlerContext(S3HandlerContextKeys.IS_PAYLOAD_SIGNING_ENABLED);
170             return isPayloadSigningEnabled != null && isPayloadSigningEnabled;
171         }
172         return false;
173     }
174
175     /**
176      * Read the content of the request to get the length of the stream. This
177      * method will wrap the stream by SdkBufferedInputStream if it is not
178      * mark-supported.
179      */

180     static long getContentLength(SignableRequest<?> request) throws IOException {
181         final InputStream content = request.getContent();
182         if (!content.markSupported())
183             throw new IllegalStateException("Bug: request input stream must have been made mark-and-resettable at this point");
184         ReadLimitInfo info = request.getReadLimitInfo();
185         final int readLimit = info.getReadLimit();
186         long contentLength = 0;
187         byte[] tmp = new byte[4096];
188         int read;
189         content.mark(readLimit);
190         while ((read = content.read(tmp)) != -1) {
191             contentLength += read;
192         }
193         try {
194             content.reset();
195         } catch(IOException ex) {
196             throw new ResetException("Failed to reset the input stream", ex);
197         }
198         return contentLength;
199     }
200 }
201