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.StringUtils;
43 import software.amazon.awssdk.utils.http.SdkHttpUtils;
44 
45 /**
46  * Responsible for creating Apache HttpClient 4 request objects.
47  */
48 @SdkInternalApi
49 public class ApacheHttpRequestFactory {
50 
51     private static final List<String> IGNORE_HEADERS = Arrays.asList(HttpHeaders.CONTENT_LENGTH, HttpHeaders.HOST,
52                                                                      HttpHeaders.TRANSFER_ENCODING);
53 
create(final HttpExecuteRequest request, final ApacheHttpRequestConfig requestConfig)54     public HttpRequestBase create(final HttpExecuteRequest request, final ApacheHttpRequestConfig requestConfig) {
55         HttpRequestBase base = createApacheRequest(request, sanitizeUri(request.httpRequest()));
56         addHeadersToRequest(base, request.httpRequest());
57         addRequestConfig(base, request.httpRequest(), requestConfig);
58 
59         return base;
60     }
61 
62     /**
63      * The Apache HTTP client doesn't allow consecutive slashes in the URI. For S3
64      * and other AWS services, this is allowed and required. This methods replaces
65      * any occurrence of "//" in the URI path with "/%2F".
66      *
67      * @see SdkHttpRequest#getUri()
68      * @param request The existing request
69      * @return a new String containing the modified URI
70      */
sanitizeUri(SdkHttpRequest request)71     private URI sanitizeUri(SdkHttpRequest request) {
72         String path = request.encodedPath();
73         if (path.contains("//")) {
74             int port = request.port();
75             String protocol = request.protocol();
76             String newPath = StringUtils.replace(path, "//", "/%2F");
77             String encodedQueryString = request.encodedQueryParameters().map(value -> "?" + value).orElse("");
78 
79             // Do not include the port in the URI when using the default port for the protocol.
80             String portString = SdkHttpUtils.isUsingStandardPort(protocol, port) ?
81                                 "" : ":" + port;
82 
83             return URI.create(protocol + "://" + request.host() + portString + newPath + encodedQueryString);
84         }
85 
86         return request.getUri();
87     }
88 
addRequestConfig(final HttpRequestBase base, final SdkHttpRequest request, final ApacheHttpRequestConfig requestConfig)89     private void addRequestConfig(final HttpRequestBase base,
90                                   final SdkHttpRequest request,
91                                   final ApacheHttpRequestConfig requestConfig) {
92         int connectTimeout = saturatedCast(requestConfig.connectionTimeout().toMillis());
93         int connectAcquireTimeout = saturatedCast(requestConfig.connectionAcquireTimeout().toMillis());
94         RequestConfig.Builder requestConfigBuilder = RequestConfig
95                 .custom()
96                 .setConnectionRequestTimeout(connectAcquireTimeout)
97                 .setConnectTimeout(connectTimeout)
98                 .setSocketTimeout(saturatedCast(requestConfig.socketTimeout().toMillis()))
99                 .setLocalAddress(requestConfig.localAddress());
100 
101         ApacheUtils.disableNormalizeUri(requestConfigBuilder);
102 
103         /*
104          * Enable 100-continue support for PUT operations, since this is
105          * where we're potentially uploading large amounts of data and want
106          * to find out as early as possible if an operation will fail. We
107          * don't want to do this for all operations since it will cause
108          * extra latency in the network interaction.
109          */
110         if (SdkHttpMethod.PUT == request.method() && requestConfig.expectContinueEnabled()) {
111             requestConfigBuilder.setExpectContinueEnabled(true);
112         }
113 
114         base.setConfig(requestConfigBuilder.build());
115     }
116 
117 
createApacheRequest(HttpExecuteRequest request, URI uri)118     private HttpRequestBase createApacheRequest(HttpExecuteRequest request, URI uri) {
119         switch (request.httpRequest().method()) {
120             case HEAD:
121                 return new HttpHead(uri);
122             case GET:
123                 return new HttpGet(uri);
124             case DELETE:
125                 return new HttpDelete(uri);
126             case OPTIONS:
127                 return new HttpOptions(uri);
128             case PATCH:
129                 return wrapEntity(request, new HttpPatch(uri));
130             case POST:
131                 return wrapEntity(request, new HttpPost(uri));
132             case PUT:
133                 return wrapEntity(request, new HttpPut(uri));
134             default:
135                 throw new RuntimeException("Unknown HTTP method name: " + request.httpRequest().method());
136         }
137     }
138 
wrapEntity(HttpExecuteRequest request, HttpEntityEnclosingRequestBase entityEnclosingRequest)139     private HttpRequestBase wrapEntity(HttpExecuteRequest request,
140                                        HttpEntityEnclosingRequestBase entityEnclosingRequest) {
141 
142         /*
143          * We should never reuse the entity of the previous request, since
144          * reading from the buffered entity will bypass reading from the
145          * original request content. And if the content contains InputStream
146          * wrappers that were added for validation-purpose (e.g.
147          * Md5DigestCalculationInputStream), these wrappers would never be
148          * read and updated again after AmazonHttpClient resets it in
149          * preparation for the retry. Eventually, these wrappers would
150          * return incorrect validation result.
151          */
152         if (request.contentStreamProvider().isPresent()) {
153             HttpEntity entity = new RepeatableInputStreamRequestEntity(request);
154             if (!request.httpRequest().firstMatchingHeader(HttpHeaders.CONTENT_LENGTH).isPresent() && !entity.isChunked()) {
155                 entity = ApacheUtils.newBufferedHttpEntity(entity);
156             }
157             entityEnclosingRequest.setEntity(entity);
158         }
159 
160         return entityEnclosingRequest;
161     }
162 
163     /**
164      * Configures the headers in the specified Apache HTTP request.
165      */
addHeadersToRequest(HttpRequestBase httpRequest, SdkHttpRequest request)166     private void addHeadersToRequest(HttpRequestBase httpRequest, SdkHttpRequest request) {
167         httpRequest.addHeader(HttpHeaders.HOST, getHostHeaderValue(request));
168 
169         // Copy over any other headers already in our request
170         request.forEachHeader((name, value) -> {
171             // HttpClient4 fills in the Content-Length header and complains if
172             // it's already present, so we skip it here. We also skip the Host
173             // header to avoid sending it twice, which will interfere with some
174             // signing schemes.
175             if (!IGNORE_HEADERS.contains(name)) {
176                 for (String headerValue : value) {
177                     httpRequest.addHeader(name, headerValue);
178                 }
179             }
180         });
181     }
182 
getHostHeaderValue(SdkHttpRequest request)183     private String getHostHeaderValue(SdkHttpRequest request) {
184         // Apache doesn't allow us to include the port in the host header if it's a standard port for that protocol. For that
185         // reason, we don't include the port when we sign the message. See {@link SdkHttpRequest#port()}.
186         return !SdkHttpUtils.isUsingStandardPort(request.protocol(), request.port())
187                 ? request.host() + ":" + request.port()
188                 : request.host();
189     }
190 }
191