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