1 /*
2  * Copyright 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
16 package software.amazon.awssdk.http.apache.internal.impl;
17
18 import static software.amazon.awssdk.utils.NumericUtils.saturatedCast;
19
20 import java.net.URI;
21 import java.util.Arrays;
22 import java.util.List;
23 import org.apache.http.HttpEntity;
24 import org.apache.http.HttpHeaders;
25 import org.apache.http.client.config.RequestConfig;
26 import org.apache.http.client.methods.HttpDelete;
27 import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
28 import org.apache.http.client.methods.HttpGet;
29 import org.apache.http.client.methods.HttpHead;
30 import org.apache.http.client.methods.HttpOptions;
31 import org.apache.http.client.methods.HttpPatch;
32 import org.apache.http.client.methods.HttpPost;
33 import org.apache.http.client.methods.HttpPut;
34 import org.apache.http.client.methods.HttpRequestBase;
35 import software.amazon.awssdk.annotations.SdkInternalApi;
36 import software.amazon.awssdk.http.HttpExecuteRequest;
37 import software.amazon.awssdk.http.SdkHttpMethod;
38 import software.amazon.awssdk.http.SdkHttpRequest;
39 import software.amazon.awssdk.http.apache.internal.ApacheHttpRequestConfig;
40 import software.amazon.awssdk.http.apache.internal.RepeatableInputStreamRequestEntity;
41 import software.amazon.awssdk.http.apache.internal.utils.ApacheUtils;
42 import software.amazon.awssdk.utils.http.SdkHttpUtils;
43
44 /**
45  * Responsible for creating Apache HttpClient 4 request objects.
46  */

47 @SdkInternalApi
48 public class ApacheHttpRequestFactory {
49
50     private static final String DEFAULT_ENCODING = "UTF-8";
51
52     private static final List<String> IGNORE_HEADERS = Arrays.asList(HttpHeaders.CONTENT_LENGTH, HttpHeaders.HOST);
53
54     public HttpRequestBase create(final HttpExecuteRequest request, final ApacheHttpRequestConfig requestConfig) {
55         URI uri = request.httpRequest().getUri();
56
57         HttpRequestBase base = createApacheRequest(request, sanitizeUri(uri));
58         addHeadersToRequest(base, request.httpRequest());
59         addRequestConfig(base, request.httpRequest(), requestConfig);
60
61         return base;
62     }
63
64     /**
65      * The Apache HTTP client doesn't allow consecutive slashes in the URI. For S3
66      * and other AWS services, this is allowed and required. This methods replaces
67      * any occurrence of "//" in the URI path with "/%2F".
68      *
69      * @param uri The existing URI with double slashes not sanitized for Apache.
70      * @return a new String containing the modified URI
71      */

72     private String sanitizeUri(URI uri) {
73         String newPath = uri.getPath().replace("//""/%2F");
74         return uri.toString().replace(uri.getPath(), newPath);
75     }
76
77     private void addRequestConfig(final HttpRequestBase base,
78                                   final SdkHttpRequest request,
79                                   final ApacheHttpRequestConfig requestConfig) {
80         int connectTimeout = saturatedCast(requestConfig.connectionTimeout().toMillis());
81         int connectAcquireTimeout = saturatedCast(requestConfig.connectionAcquireTimeout().toMillis());
82         RequestConfig.Builder requestConfigBuilder = RequestConfig
83                 .custom()
84                 .setConnectionRequestTimeout(connectAcquireTimeout)
85                 .setConnectTimeout(connectTimeout)
86                 .setSocketTimeout(saturatedCast(requestConfig.socketTimeout().toMillis()))
87                 .setLocalAddress(requestConfig.localAddress());
88
89         ApacheUtils.disableNormalizeUri(requestConfigBuilder);
90
91         /*
92          * Enable 100-continue support for PUT operations, since this is
93          * where we're potentially uploading large amounts of data and want
94          * to find out as early as possible if an operation will fail. We
95          * don't want to do this for all operations since it will cause
96          * extra latency in the network interaction.
97          */

98         if (SdkHttpMethod.PUT == request.method() && requestConfig.expectContinueEnabled()) {
99             requestConfigBuilder.setExpectContinueEnabled(true);
100         }
101
102         base.setConfig(requestConfigBuilder.build());
103     }
104
105
106     private HttpRequestBase createApacheRequest(HttpExecuteRequest request, String uri) {
107         switch (request.httpRequest().method()) {
108             case HEAD:
109                 return new HttpHead(uri);
110             case GET:
111                 return new HttpGet(uri);
112             case DELETE:
113                 return new HttpDelete(uri);
114             case OPTIONS:
115                 return new HttpOptions(uri);
116             case PATCH:
117                 return wrapEntity(request, new HttpPatch(uri));
118             case POST:
119                 return wrapEntity(request, new HttpPost(uri));
120             case PUT:
121                 return wrapEntity(request, new HttpPut(uri));
122             default:
123                 throw new RuntimeException("Unknown HTTP method name: " + request.httpRequest().method());
124         }
125     }
126
127     private HttpRequestBase wrapEntity(HttpExecuteRequest request,
128                                        HttpEntityEnclosingRequestBase entityEnclosingRequest) {
129
130         /*
131          * We should never reuse the entity of the previous request, since
132          * reading from the buffered entity will bypass reading from the
133          * original request content. And if the content contains InputStream
134          * wrappers that were added for validation-purpose (e.g.
135          * Md5DigestCalculationInputStream), these wrappers would never be
136          * read and updated again after AmazonHttpClient resets it in
137          * preparation for the retry. Eventually, these wrappers would
138          * return incorrect validation result.
139          */

140         if (request.contentStreamProvider().isPresent()) {
141             HttpEntity entity = new RepeatableInputStreamRequestEntity(request);
142             if (request.httpRequest().headers().get(HttpHeaders.CONTENT_LENGTH) == null) {
143                 entity = ApacheUtils.newBufferedHttpEntity(entity);
144             }
145             entityEnclosingRequest.setEntity(entity);
146         }
147
148         return entityEnclosingRequest;
149     }
150
151     /**
152      * Configures the headers in the specified Apache HTTP request.
153      */

154     private void addHeadersToRequest(HttpRequestBase httpRequest, SdkHttpRequest request) {
155
156         httpRequest.addHeader(HttpHeaders.HOST, getHostHeaderValue(request));
157
158
159         // Copy over any other headers already in our request
160         request.headers().entrySet().stream()
161                /*
162                 * HttpClient4 fills in the Content-Length header and complains if
163                 * it's already present, so we skip it here. We also skip the Host
164                 * header to avoid sending it twice, which will interfere with some
165                 * signing schemes.
166                 */

167                .filter(e -> !IGNORE_HEADERS.contains(e.getKey()))
168                .forEach(e -> e.getValue().forEach(h -> httpRequest.addHeader(e.getKey(), h)));
169     }
170
171     private String getHostHeaderValue(SdkHttpRequest request) {
172         // Apache doesn't allow us to include the port in the host header if it's a standard port for that protocol. For that
173         // reason, we don't include the port when we sign the message. See {@link SdkHttpRequest#port()}.
174         return !SdkHttpUtils.isUsingStandardPort(request.protocol(), request.port())
175                 ? request.host() + ":" + request.port()
176                 : request.host();
177     }
178 }
179