1 /*
2  * Copyright (c) 2016-2019. 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.http.apache.request.impl;
16
17 import com.amazonaws.ClientConfiguration;
18 import com.amazonaws.ProxyAuthenticationMethod;
19 import com.amazonaws.Request;
20 import com.amazonaws.SdkClientException;
21 import com.amazonaws.handlers.HandlerContextKey;
22 import com.amazonaws.http.HttpMethodName;
23 import com.amazonaws.http.RepeatableInputStreamRequestEntity;
24 import com.amazonaws.http.apache.utils.ApacheUtils;
25 import com.amazonaws.http.request.HttpRequestFactory;
26 import com.amazonaws.http.settings.HttpClientSettings;
27 import com.amazonaws.util.FakeIOException;
28 import com.amazonaws.util.SdkHttpUtils;
29 import java.net.URI;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.List;
33 import java.util.Map.Entry;
34 import org.apache.http.HttpEntity;
35 import org.apache.http.HttpHeaders;
36 import org.apache.http.client.config.AuthSchemes;
37 import org.apache.http.client.config.RequestConfig;
38 import org.apache.http.client.methods.HttpDelete;
39 import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
40 import org.apache.http.client.methods.HttpHead;
41 import org.apache.http.client.methods.HttpOptions;
42 import org.apache.http.client.methods.HttpPatch;
43 import org.apache.http.client.methods.HttpPost;
44 import org.apache.http.client.methods.HttpPut;
45 import org.apache.http.client.methods.HttpRequestBase;
46 import org.apache.http.entity.BufferedHttpEntity;
47
48 /**
49  * Responsible for creating Apache HttpClient 4 request objects.
50  */

51 public class ApacheHttpRequestFactory implements
52         HttpRequestFactory<HttpRequestBase> {
53
54     private static final String DEFAULT_ENCODING = "UTF-8";
55
56     private static final List<String> ignoreHeaders = Arrays.asList
57             (HttpHeaders.CONTENT_LENGTH, HttpHeaders.HOST);
58
59     @Override
60     public HttpRequestBase create(final Request<?> request,
61                                   final HttpClientSettings settings)
62             throws
63             FakeIOException {
64         URI endpoint = request.getEndpoint();
65
66         String uri;
67         // skipAppendUriPath is set for APIs making requests with presigned urls. Otherwise
68         // a slash will be appended at the end and the request will fail
69         if (request.getOriginalRequest().getRequestClientOptions().isSkipAppendUriPath()) {
70             uri = endpoint.toString();
71         } else {
72             /*
73              * HttpClient cannot handle url in pattern of "http://host//path", so we
74              * have to escape the double-slash between endpoint and resource-path
75              * into "/%2F"
76              */

77             uri = SdkHttpUtils.appendUri(endpoint.toString(), request.getResourcePath(), true);
78         }
79
80         String encodedParams = SdkHttpUtils.encodeParameters(request);
81
82         /*
83          * For all non-POST requests, and any POST requests that already have a
84          * payload, we put the encoded params directly in the URI, otherwise,
85          * we'll put them in the POST request's payload.
86          */

87         boolean requestHasNoPayload = request.getContent() != null;
88         boolean requestIsPost = request.getHttpMethod() == HttpMethodName.POST;
89         boolean putParamsInUri = !requestIsPost || requestHasNoPayload;
90         if (encodedParams != null && putParamsInUri) {
91             uri += "?" + encodedParams;
92         }
93
94         final HttpRequestBase base = createApacheRequest(request, uri, encodedParams);
95         addHeadersToRequest(base, request);
96         addRequestConfig(base, request, settings);
97
98         return base;
99     }
100
101     private void addRequestConfig(final HttpRequestBase base,
102                                   final Request<?> request,
103                                   final HttpClientSettings settings) {
104         final RequestConfig.Builder requestConfigBuilder = RequestConfig
105                 .custom()
106                 .setConnectionRequestTimeout(settings.getConnectionPoolRequestTimeout())
107                 .setConnectTimeout(settings.getConnectionTimeout())
108                 .setSocketTimeout(settings.getSocketTimeout())
109                 .setLocalAddress(settings.getLocalAddress());
110
111         ApacheUtils.disableNormalizeUri(requestConfigBuilder);
112
113         /*
114          * Enable 100-continue support for PUT operations, since this is
115          * where we're potentially uploading large amounts of data and want
116          * to find out as early as possible if an operation will fail. We
117          * don't want to do this for all operations since it will cause
118          * extra latency in the network interaction.
119          */

120         if (HttpMethodName.PUT == request.getHttpMethod() && settings.isUseExpectContinue()) {
121             requestConfigBuilder.setExpectContinueEnabled(true);
122         }
123
124         addProxyConfig(requestConfigBuilder, settings);
125
126         base.setConfig(requestConfigBuilder.build());
127     }
128
129     private HttpRequestBase createApacheRequest(Request<?> request, String uri, String encodedParams) throws FakeIOException {
130         switch (request.getHttpMethod()) {
131             case HEAD:
132                 return new HttpHead(uri);
133             case GET:
134                 return wrapEntity(request, new HttpGetWithBody(uri), encodedParams);
135             case DELETE:
136                 return new HttpDelete(uri);
137             case OPTIONS:
138                 return new HttpOptions(uri);
139             case PATCH:
140                 return wrapEntity(request, new HttpPatch(uri), encodedParams);
141             case POST:
142                 return wrapEntity(request, new HttpPost(uri), encodedParams);
143             case PUT:
144                 return wrapEntity(request, new HttpPut(uri), encodedParams);
145             default:
146                 throw new SdkClientException("Unknown HTTP method name: " + request.getHttpMethod());
147         }
148     }
149
150
151     /**
152      * If SDK want to set Content-Length header if it missing on the request, wrap the http entity with
153      * {@link BufferedHttpEntity} as this will buffer the data and set the Content-Length header.
154      *
155      * Otherwise use the {@link RepeatableInputStreamRequestEntity} and Apache http client will
156      * set the proper header (Content-Length or Transfer-Encoding) based on whether it can find content length
157      * from the input stream. This is fine as services accept both headers.
158      */

159     private HttpRequestBase wrapEntity(Request<?> request,
160                                        HttpEntityEnclosingRequestBase entityEnclosingRequest,
161                                        String encodedParams) throws FakeIOException {
162
163         if (HttpMethodName.POST == request.getHttpMethod()) {
164             /*
165              * If there isn't any payload content to include in this request,
166              * then try to include the POST parameters in the query body,
167              * otherwise, just use the query string. For all AWS Query services,
168              * the best behavior is putting the params in the request body for
169              * POST requests, but we can't do that for S3.
170              */

171             if (request.getContent() == null && encodedParams != null) {
172                 entityEnclosingRequest.setEntity(ApacheUtils.newStringEntity(encodedParams));
173             } else {
174                 createHttpEntityForPostVerb(request, entityEnclosingRequest);
175             }
176         } else {
177             /*
178              * We should never reuse the entity of the previous request, since
179              * reading from the buffered entity will bypass reading from the
180              * original request content. And if the content contains InputStream
181              * wrappers that were added for validation-purpose (e.g.
182              * Md5DigestCalculationInputStream), these wrappers would never be
183              * read and updated again after AmazonHttpClient resets it in
184              * preparation for the retry. Eventually, these wrappers would
185              * return incorrect validation result.
186              */

187             if (request.getContent() != null) {
188                 createHttpEntityForNonPostVerbs(request, entityEnclosingRequest);
189             }
190         }
191         return entityEnclosingRequest;
192     }
193
194     /**
195      * For POST APIs, only use buffered entity if requiresLength trait is present.
196      *
197      * The behavior difference for POST vs non-POST APIs is to ensure only minimal changes are made to header behavior
198      * (after adding requiresLength trait) and reduce the impact radius.
199      */

200     private void createHttpEntityForPostVerb(Request<?> request,
201                                              HttpEntityEnclosingRequestBase entityEnclosingRequest) throws FakeIOException {
202         HttpEntity entity = new RepeatableInputStreamRequestEntity(request);
203
204         if (request.getHeaders().get(HttpHeaders.CONTENT_LENGTH) == null && isRequiresLength(request)) {
205             entity = ApacheUtils.newBufferedHttpEntity(entity);
206         }
207
208         entityEnclosingRequest.setEntity(entity);
209     }
210
211
212     /**
213      * For non-POST APIs, use buffered entity if op is either
214      * (a) No Streaming Input or
215      * (b) hasStreamingInput and requiresLength header is present
216      *
217      * The behavior difference for POST vs non-POST APIs is to ensure only minimal changes are made to header behavior
218      * (after adding requiresLength trait) and reduce the impact radius.
219      */

220     private void createHttpEntityForNonPostVerbs(Request<?> request,
221                                                  HttpEntityEnclosingRequestBase entityEnclosingRequest) throws FakeIOException {
222
223         HttpEntity entity = new RepeatableInputStreamRequestEntity(request);
224
225         if (request.getHeaders().get(HttpHeaders.CONTENT_LENGTH) == null) {
226             if (isRequiresLength(request) || !hasStreamingInput(request)) {
227                 entity = ApacheUtils.newBufferedHttpEntity(entity);
228             }
229         }
230
231         entityEnclosingRequest.setEntity(entity);
232     }
233
234     private boolean isRequiresLength(Request<?> request) {
235         return Boolean.TRUE.equals(request.getHandlerContext(HandlerContextKey.REQUIRES_LENGTH));
236     }
237
238     private boolean hasStreamingInput(Request<?> request) {
239         return Boolean.TRUE.equals(request.getHandlerContext(HandlerContextKey.HAS_STREAMING_INPUT));
240     }
241
242     /**
243      * Configures the headers in the specified Apache HTTP request.
244      */

245     private void addHeadersToRequest(HttpRequestBase httpRequest, Request<?> request) {
246
247         httpRequest.addHeader(HttpHeaders.HOST, getHostHeaderValue(request.getEndpoint()));
248
249         // Copy over any other headers already in our request
250         for (Entry<String, String> entry : request.getHeaders().entrySet()) {
251             /*
252              * HttpClient4 fills in the Content-Length header and complains if
253              * it's already present, so we skip it here. We also skip the Host
254              * header to avoid sending it twice, which will interfere with some
255              * signing schemes.
256              */

257             if (!(ignoreHeaders.contains(entry.getKey()))) {
258                 httpRequest.addHeader(entry.getKey(), entry.getValue());
259             }
260         }
261
262         /* Set content type and encoding */
263         if (httpRequest.getHeaders(HttpHeaders.CONTENT_TYPE) == null || httpRequest
264                 .getHeaders
265                         (HttpHeaders.CONTENT_TYPE).length == 0) {
266             httpRequest.addHeader(HttpHeaders.CONTENT_TYPE,
267                     "application/x-www-form-urlencoded; " +
268                             "charset=" + DEFAULT_ENCODING.toLowerCase());
269         }
270     }
271
272     private String getHostHeaderValue(final URI endpoint) {
273         /*
274          * Apache HttpClient omits the port number in the Host header (even if
275          * we explicitly specify it) if it's the default port for the protocol
276          * in use. To ensure that we use the same Host header in the request and
277          * in the calculated string to sign (even if Apache HttpClient changed
278          * and started honoring our explicit host with endpoint), we follow this
279          * same behavior here and in the QueryString signer.
280          */

281         return SdkHttpUtils.isUsingNonDefaultPort(endpoint)
282                 ? endpoint.getHost() + ":" + endpoint.getPort()
283                 : endpoint.getHost();
284     }
285
286     /**
287      * Update the provided request configuration builder to specify the proxy authentication schemes that should be used when
288      * authenticating against the HTTP proxy.
289      *
290      * @see ClientConfiguration#setProxyAuthenticationMethods(List)
291      */

292     private void addProxyConfig(RequestConfig.Builder requestConfigBuilder, HttpClientSettings settings) {
293         if (settings.isProxyEnabled() && settings.isAuthenticatedProxy() && settings.getProxyAuthenticationMethods() != null) {
294             List<String> apacheAuthenticationSchemes = new ArrayList<String>();
295
296             for (ProxyAuthenticationMethod authenticationMethod : settings.getProxyAuthenticationMethods()) {
297                 apacheAuthenticationSchemes.add(toApacheAuthenticationScheme(authenticationMethod));
298             }
299
300             requestConfigBuilder.setProxyPreferredAuthSchemes(apacheAuthenticationSchemes);
301         }
302     }
303
304     /**
305      * Convert the customer-facing authentication method into an apache-specific authentication method.
306      */

307     private String toApacheAuthenticationScheme(ProxyAuthenticationMethod authenticationMethod) {
308         if (authenticationMethod == null) {
309             throw new IllegalStateException("The configured proxy authentication methods must not be null.");
310         }
311
312         switch (authenticationMethod) {
313             case NTLM: return AuthSchemes.NTLM;
314             case BASIC: return AuthSchemes.BASIC;
315             case DIGEST: return AuthSchemes.DIGEST;
316             case SPNEGO: return AuthSchemes.SPNEGO;
317             case KERBEROS: return AuthSchemes.KERBEROS;
318             defaultthrow new IllegalStateException("Unknown authentication scheme: " + authenticationMethod);
319         }
320     }
321 }